Reference

Connecting Stripe

Most of the outcomes that matter — paid, upgraded, churned, refunded, disputed — already live in Stripe. Connect it once and they arrive as funnel events, joined to the decision that let each customer in.

Step 1: put the trace id on the Stripe customer

When you create the Stripe customer for someone who came through /v1/decide, store that decision’s trace_id in the customer’s metadata under exactly this key:

// at signup
const d = await sortance.decide(email)          // d.trace_id === "dc_01J..."

// when you create the Stripe customer
await stripe.customers.create({
  email,
  metadata: { sortance_trace_id: d.trace_id },
})

That makes the join exact. Sortance accepts the id only if that trace belongs to your account — anything can be typed into Stripe metadata, and a mistyped or foreign id must not attach your payment to someone else’s decision.

Step 2: create a restricted key (optional, recommended)

In Stripe, create a restricted key with read access only:

  • Customers — Read
  • Subscriptions — Read
  • Invoices — Read
  • Charges — Read
  • Disputes — Read
  • Nothing with write access.

Sortance refuses a secret key outright: only rk_live_… or rk_test_… is accepted. With no key at all the webhook still works; you lose the backfill and the customer lookups below.

What the worker actually calls with the key today:

  • GET /v1/customers and GET /v1/subscriptions?status=all, paged, for the backfill.
  • GET /v1/customers/:id, once per customer it has not seen, to read the trace id and email for the join.
  • GET /v1/charges/:id, for a dispute that names a charge but not a customer.

Invoice and dispute data otherwise arrives through the webhook, not the key.

Step 3: connect, then add the webhook in Stripe

POST /v1/integrations/stripe
{
  "webhook_secret": "whsec_...",      // required: the endpoint's signing secret
  "restricted_key": "rk_live_...",    // optional
  "rotate_token": false               // optional: true mints a new webhook URL
}

// 201
{
  "integration": { "provider": "stripe", "kind": "outcome_source", "status": "connected",
                   "config": { "webhook_token_set": true }, "has_secret": true,
                   "last_event_at": null, "created_at": 1759400000 },
  "webhook_url": "https://api.sortance.com/v1/integrations/stripe/webhook/whk_..."
}
  • The webhook_url is shown once. A reconnect that keeps the existing token returns null, and the URL already in Stripe keeps working.
  • In Stripe, add that URL as a webhook endpoint subscribed to the events in the table below.
  • The order is awkward, and here is the way through. Stripe issues the whsec_ signing secret only once the endpoint exists, and the endpoint needs the URL that connecting returns. So connect twice: first with any value of the form whsec_ plus at least eight letters or digits to get the URL, then create the endpoint in Stripe and connect again with the real secret. The second call replaces the secret and keeps the same URL. Deliveries in between fail verification with a 400 and are retried by Stripe.
  • Secrets are encrypted at rest and never returned by the API. The token in the URL is stored only as its hash.
  • Every delivery is verified with HMAC-SHA256 over the raw body and Stripe’s timestamp, with a five-minute tolerance.

Events Sortance uses

Stripe eventBecomesWhen
customer.created—Teaches the join: remembers the customer's sortance_trace_id and a hash of their email. No funnel event.
customer.subscription.createdsubscribedWhen the subscription is active or past_due with a non-zero amount. Trials do not count.
customer.subscription.updatedsubscribed or upgradedsubscribed when a trial or incomplete subscription becomes active; upgraded when the monthly amount strictly rises. A downgrade maps to nothing.
customer.subscription.deletedchurnedAlways, with the cancellation reason when Stripe gives one.
invoice.paidsubscribedA paid amount above zero on a subscription. Counted once per subscription, however many paths report it.
charge.refundedrefundedAlways.
charge.dispute.createdfraud_flagAlways, with the dispute reason kept so non-fraud disputes can be filtered.

Properties kept on each event are an allow-list of non-identifying fields: Stripe object ids, status, amounts, currency, livemode and reasons. Email addresses on Stripe objects are hashed for the join and then discarded.

How an event is joined to a decision

  1. exact — a sortance_trace_id on the object or on its customer (remembered from customer.created, or looked up with your restricted key) that matches a trace on your account.
  2. by_email — otherwise, the customer’s email, hashed, matched to your most recent /v1/decide for that hash in the 90 days before the event.
  3. none — otherwise. The event is kept, not dropped.
The by-email join is approximate

Someone who signs up as mia@acme.com and pays as billing@acme.com is not matched, and a shared address can be matched to the wrong signup. Sortance cannot tell these cases apart, so every event records its join_method and segment reports count exact, by-email and unmatched joins separately. If you want numbers you can rely on, set metadata.sortance_trace_id.

Backfill

When you connect with a restricted key, Sortance pages through your existing customers (to learn trace ids and email hashes) and then your subscriptions (to record past subscribed and churned events), 100 objects a page, so segment reports are useful on day one. Pages are idempotent: a conversion is never counted twice, even where the backfill overlaps live webhooks.

One limit to know: each connect call processes at most five pages — 500 objects — and saves its place. Continuing a larger backfill automatically is not wired yet; posting the connect call again with the restricted key resumes from where it stopped. If the key is revoked or missing a permission, the integration’s status becomes error.

Endpoints

MethodPathDoes
GET/v1/integrations/stripeConnection status: provider, status (connected, error, revoked), has_secret, last_event_at. Never the secret.
POST/v1/integrations/stripeConnect. Returns the webhook URL once.
DELETE/v1/integrations/stripeRevoke: deletes the stored secrets and the webhook token.
POST/v1/integrations/stripe/webhook/:account_tokenWhere Stripe sends events. Authenticated by the token and Stripe's signature, not by your API key.

Webhook responses (to Stripe)

StatusMeans
200Verified. { received, ignored, event, join_method } — ignored is true for an event type Sortance does not use.
400Bad or missing Stripe-Signature, or a timestamp outside the five-minute tolerance. Usually the stored signing secret is not this endpoint's; connect again with the right one.
404Unknown or revoked webhook token.
500Sortance could not decrypt your signing secret or store the event. Stripe retries, so nothing is lost once it is fixed.

The connect call returns 400 invalid_request for a missing or malformed webhook_secret or a key that is not a restricted key, and 404 not_found from GET or DELETE when Stripe is not connected.

Not yet

Stripe Connect — one click, revocable from the Stripe side, no key to paste — needs Stripe’s platform review and is not built. The restricted key plus webhook secret is the only way in today.