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.

The shift. Every product is about to get extraordinary, and the ones that know their users will beat the ones that don’t. These docs show what you can build the moment a user connects.

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:

1

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.

2

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.

3

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.

4

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.

Everything below is a real request. Base URL, credentials and the full reference are shared once you’re set up. Book a call to get started.

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.

POST /api/onboarding/links
// 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.

Completion webhook
// 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)
What you build. A connect flow with no redirect handling and no UI of your own. Verify the HMAC, exchange the code, store the refresh token against your own user id.

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.

A template can name the services it needs. If the user hasn’t connected them, the call returns 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.

POST /api/data/insights/on-demand
// 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.
What you build. Map 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.

POST /api/data/insights/on-demand
// 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"
}
What you build. Seed onboarding and your recommender from the first screen. No blank slate, no pick your interests wall.

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.

POST /api/data/insights/on-demand
// 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"
}
What you build. Threshold on 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.

POST /api/data/insights/on-demand
// 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"
}
What you build. A short, typed, confidence-scored brief your agent can trust. It behaves like it has known the user for years.

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.

GET /api/data/normalized?service=monzo&limit=1
// 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"
}
One scope per service, {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.

New connectors ship regularly, and each arrives as one more scope on the same endpoints. A user’s context gets richer over time, without you integrating anything new.

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.

POST /api/portal/sessions
// 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"
}
What you build. A Manage connected apps button. Pass 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.

AES-256GDPRSOC 2 Type IIHIPAA · in progress
Revocable by default. Access is granted by the user, scoped to what they approve, and gone the moment they withdraw it. You build on permission, never on possession.

Who’s behind this

A note from the founders

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.

HN
Harry & NikolaFounders, Weve

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.

Integration guide & API reference
Shared with approved partners
Book a call to get set up
A walkthrough of the flow for your exact use case
Your client credentials and sandbox access
The full OpenAPI reference, once you’re set up