Build products that already know your users
Weve is one permissioned API across the apps your users already trust. Connect once, ask the questions your product actually needs answered, and get back typed answers you can act on, offered by the user, on their terms.
Traditional personalization starts from zero and slowly learns. Weve starts from everything: the calendar, music, fitness, spending and health signals a user already generates, normalized into one revocable stream. You don’t stitch a dozen integrations or write a schema per provider. You declare a question and the shape of the answer you can handle; weve reads across the user’s connected services and returns exactly that shape, with a confidence score and the services it drew on.
How it works
Weve sits between your product and the apps your users already trust. You never touch raw provider APIs or hold provider credentials. Four steps, start to finish:
The user connects
Through weve’s OAuth 2.0 consent screen, or through an onboarding link you mint for someone who has never heard of weve. They choose which services to connect and which scopes to grant. The passport is theirs.
You define the question
A prompt template says what to look at and how far back; typed questions say exactly what shape the answer must take. Both are yours, created once and reused for every user.
Weve answers
One POST returns an answer per question, each validated against the type you declared, each with a confidence score, and each free to come back unknown with a reason rather than guess.
They stay in control
Scopes are granted per service and revocable at any time, from a weve-hosted portal you can drop them into. You build on permission, never on possession.
Connecting a user
If your user already has a weve account, send them through the standard OAuth 2.0 authorization code flow with PKCE and exchange the code for an access token. If they don’t, weve will do the whole thing for you: mint an onboarding link, send it however you already talk to them, and weve handles sign-up, service connection and consent on hosted pages.
// mint a link for a customer who has never heard of weve { "externalId": "imsg:+14155550123", "scopes": [ "openid", "offline_access", "insights.on-demand", "google-calendar.normalized", "whoop.normalized" ], "returnUrl": "https://your-app.com/connected" } // weve returns — 200 OK { "url": "https://platform.weve.dev/onboard/nS3xk8Qm1pV7hQ2b0dRf9tLc4wY6uZaE", "expiresAt": "2026-08-21T09:14:00.000Z", "externalId": "imsg:+14155550123" }
That call authenticates as your client — HTTP Basic with your client_id and client_secret — because there is no user token yet. The externalId is whatever you already call this customer; it comes back to you untouched.
// POST to your configured webhook when the customer finishes // header: x-weve-signature: t=1755679440,v1=<hex hmac-sha256 of "t.body"> { "type": "onboarding.completed", "externalId": "imsg:+14155550123", "clientId": "wv_client_8f2a1c", "code": "TmV2ZXJHb25uYUdpdmVZb3VVcA", "redirectUri": "https://platform.weve.dev/onboard/complete", "codeVerifier": "b1Qy7Zk0Rr8sVn2Ld6Xa4Tc9Uw3Ee5Ii7Oo1Pp2Qq3", "scopes": ["openid", "offline_access", "insights.on-demand", "google-calendar.normalized", "whoop.normalized"], "completedAt": "2026-08-20T09:14:00.000Z" } // exchange it at POST /api/auth/oauth2/token // grant_type=authorization_code, code, redirect_uri and code_verifier // (weve started the grant, so the verifier is weve's — pass it back verbatim)
Asking a question
Every question you send declares the shape of the answer you can handle. Weve validates the answer against that shape before it reaches you, so a value outside your options is impossible: it comes back as unknown: true with a reason instead. That is the whole contract, and it is why the responses below drop straight into typed code.
yes_no
A boolean. Branch on it directly.
select_one
One of the options you declared. Never anything else.
multi_select
A subset of your options, with optional min and max selections.
number · number_in_range
A number, bounded to the range you set.
date · date_range
YYYY-MM-DD, or a from/to pair.
free_text
Prose, capped at your maxLength. For the things a taxonomy cannot hold.
Questions ride on a prompt template — what to look at, and over how many days. Weve ships defaults, and you can create your own with POST /api/data/prompts; they belong to your client and nobody else sees them. Up to fifty questions per request.
409 rather than answering from thin air.Perfect timing
Ask when a user is actually reachable. Weve reads across their day — meetings, recovery, the rhythm of the last fortnight — and answers in the vocabulary your scheduler already speaks, so you stop firing notifications into the void.
// you ask { "promptTemplateId": "default-cross-service", "questions": [ { "id": "best_send_window", "prompt": "Which part of the day is this person most likely to be free and to act on a notification?", "answer": { "type": "select_one", "options": ["early_morning", "morning", "afternoon", "evening", "late_evening"] } }, { "id": "under_load", "prompt": "Is this person under unusual load right now — dense calendar, poor recovery?", "answer": { "type": "yes_no" } } ] } // weve returns — 201 Created { "id": "cmf3k91qd0007l804m2xh9v1e", "promptTemplateId": "default-cross-service", "summary": "Back-to-back meetings Tuesday through Thursday with recovery in the low 40s...", "services": ["google-calendar", "whoop", "spotify"], "windowStart": "2026-08-05T00:00:00.000Z", "windowEnd": "2026-08-20T00:00:00.000Z", "answers": [ { "questionId": "best_send_window", "value": "early_morning", "unknown": false, "confidence": 0.86 }, { "questionId": "under_load", "value": true, "unknown": false, "confidence": 0.79 } ], "model": "claude-opus-4-6" } // "questions" is echoed back alongside the answers; trimmed here.
best_send_window onto your own send slots. Because you declared the options, the value is always one you already handle.Zero cold start
Ask what a brand-new user cares about before they have done anything in your product. Declare your own taxonomy as the options and get back the subset the evidence supports. Onboarding stops being a blank slate.
// you ask { "promptTemplateId": "cmf2q7x4k0003tz8v6b1p9wjd", "questions": [ { "id": "interests", "prompt": "Which of these areas does this person show real evidence for?", "answer": { "type": "multi_select", "options": ["endurance_sport", "strength_training", "live_music", "home_cooking", "travel", "gaming"], "maxSelections": 3 } }, { "id": "onboarding_track", "prompt": "Which onboarding track fits them best?", "answer": { "type": "select_one", "options": ["athlete", "casual", "social"] } } ] } // weve returns — 201 Created { "services": ["strava", "spotify", "monzo"], "answers": [ { "questionId": "interests", "value": ["endurance_sport", "live_music", "home_cooking"], "unknown": false, "confidence": 0.81 }, { "questionId": "onboarding_track", "value": "athlete", "unknown": false, "confidence": 0.74 } ], "model": "claude-opus-4-6" }
Churn rescue
Ask whether a user is drifting before the cancel screen, and ask why. Weve reads the cross-app signal your own analytics can’t see — and tells you plainly when the data doesn’t support an answer, instead of inventing one.
// you ask { "promptTemplateId": "cmf2q7x4k0003tz8v6b1p9wjd", "questions": [ { "id": "disengagement", "prompt": "How strongly is this person disengaging compared with the previous fortnight?", "answer": { "type": "number_in_range", "min": 0, "max": 1 } }, { "id": "life_event", "prompt": "Is there a plausible life event behind the drop — travel, injury, a change of job?", "answer": { "type": "free_text", "maxLength": 240 } }, { "id": "switched_product", "prompt": "Is there evidence they have started using a competing product?", "answer": { "type": "yes_no" } } ] } // weve returns — 201 Created { "services": ["strava", "google-calendar", "monzo"], "answers": [ { "questionId": "disengagement", "value": 0.83, "unknown": false, "confidence": 0.77 }, { "questionId": "life_event", "value": "Training volume fell to zero after a run logged as a calf strain; two physio payments followed.", "unknown": false, "confidence": 0.62 }, { "questionId": "switched_product", "unknown": true, "reason": "No connected service carries app-usage data.", "confidence": 0 } ], "model": "claude-opus-4-6" }
disengagement to fire a save flow, and use life_event to write one that sounds like it was meant for them. Note the third answer: an honest unknown beats a confident guess.Agent context
Give your AI agent the user, resolved. Ask for the handful of facts it should condition on and hand them over as system context, so it acts from real understanding instead of starting cold.
// you ask { "promptTemplateId": "cmf2q7x4k0003tz8v6b1p9wjd", "questions": [ { "id": "working_pattern", "prompt": "Summarise when this person is typically working, in one line.", "answer": { "type": "free_text", "maxLength": 200 } }, { "id": "dietary", "prompt": "Which dietary constraints are evidenced by their spending and calendar?", "answer": { "type": "multi_select", "options": ["vegetarian", "vegan", "gluten_free", "none_evidenced"] } }, { "id": "typical_sleep_hours", "prompt": "How many hours does this person typically sleep on a weeknight?", "answer": { "type": "number_in_range", "min": 0, "max": 14 } } ] } // weve returns — 201 Created { "services": ["google-calendar", "monzo", "whoop"], "answers": [ { "questionId": "working_pattern", "value": "Meetings cluster 09:30–17:00 Europe/London, with a consistent 07:00–09:00 quiet block.", "unknown": false, "confidence": 0.88 }, { "questionId": "dietary", "value": ["vegetarian"], "unknown": false, "confidence": 0.71 }, { "questionId": "typical_sleep_hours", "value": 6.9, "unknown": false, "confidence": 0.84 } ], "model": "claude-opus-4-6" }
Normalized events
When you want the signal rather than an answer, read the events directly. Every service lands in one envelope — kind, title, amountMinor, currency, and a data payload whose shape is consistent per kind — so a transaction is a transaction whether it came from a bank or a card. Newest first, cursor-paginated, filterable by service, event type and time window.
// weve returns — 200 OK { "items": [ { "id": "cmf3k91qd0007l804m2xh9v1e", "service": "monzo", "eventType": "transactions", "kind": "transaction", "externalId": "tx_0000AbCdEfGhIjKl", "title": "Pret A Manger", "amountMinor": -420, "currency": "GBP", "data": { "category": "eating_out", "description": "PRET A MANGER LONDON", "settled": true, "includeInSpending": true }, "occurredAt": "2026-08-19T08:12:44.000Z", "createdAt": "2026-08-19T08:31:02.117Z" } ], "nextCursor": "cmf3k91qd0007l804m2xh9v1e" }
{service}.normalized. A token only ever sees the services its user granted, and the endpoint quietly returns nothing for the rest.Connected apps
Connect weve once instead of maintaining a wall of APIs. Your users bring the apps they already trust; weve does the normalization across categories.
Calendar & email
Google Calendar, Gmail: availability, rhythm, intent.
Music
Spotify: taste, listening rhythm, what a mood sounds like.
Fitness & health
Strava, WHOOP, Apple Health: energy, recovery, routine.
Money
Monzo: spending, merchants, habits.
Place
Location: where the day happened, and the weather there.
Messages
WhatsApp: conversations the user chooses to share.
Their own words
Notes, voice memos, photos and video captured in the weve app.
Account portal
Users need somewhere to see what they have connected and to take it back. Rather than building that screen, mint a session and redirect them to a weve-hosted one — the same pattern as a billing portal. Single-use, time-limited, and scoped to what you want them to be able to do there.
// you send — with an access token you already hold for that user { "returnUrl": "https://your-app.com/settings", "scopes": ["connections", "client.revoke"] } // weve returns — 200 OK { "url": "https://platform.weve.dev/portal/7pQ2m9Xt4bK1zR8vE3sW6uY0aC5dN", "expiresAt": "2026-08-20T09:44:00.000Z" }
connections to let them add and remove services, and client.revoke to let them cut you off entirely — which they can do elsewhere regardless, so offering it is only honest.Permissions & security
Trust isn’t a feature here; it’s the architecture. Every request runs on AES-256 encryption, row-level permissions, and immutable audit logs. Users see exactly what you’re asking for and why, and can revoke access at any time, per connector.
Who’s behind this
We’ve spent years building products, and we kept hitting the same wall: we never really knew the people we were building for. Personalization started from zero every time. You guess, you test, and you slowly learn what a good product could have known on day one.
The strange part is that the context already exists. It is scattered across the apps people use every day, locked in silos, and taken without anyone really choosing to give it. Users don’t own it, and builders can’t reach it. Everyone loses.
AI has made this matter more, not less. The products people are about to love are the ones that understand them, and an AI that knows nothing about you can only do so much. Context is the difference between a tool and something that feels like it was made for you.
Weve is the bridge we always wished we had. Users hold their own context and decide who gets it. Builders start from understanding instead of zero, with permission, never possession. That is the relationship we want between people and the products they use, and it is the one we are here to build.
Get access
Weve is in early access. The full integration guide — base URLs, the OAuth flow end to end, every endpoint and every scope — is shared with partners once you’re set up. Book a short call: we’ll walk through the flow for your use case and get you your keys.