Sphere Blog
6 min

Introducing Webhooks v2

Signed, sequenced event delivery for every customer verification and transfer state change.

Written by
Sphere Team
Published on
August 12, 2026

Money movement is asynchronous, and the interesting moments happen on someone else's clock. A compliance review completes. A destination bank settles.

Tracking a transfer's progress on Sphere used to mean asking on a timer. Webhook coverage did not extend to transfer status, so teams wrote the loop, the GET, a diff against what was stored last time, and error handling for the times the request failed. It worked, but it put a fixed floor under how quickly you could react and a steady load on an API that had nothing new to say most of the time.

Webhooks v2 is live. The delivery engine is rebuilt, the event catalog is new, and endpoints can now be managed through the API. Eighteen events cover the customer verification and transfer lifecycles, and Sphere delivers each one to your endpoint as it happens. Your handler now runs when the state changes instead of on your polling interval.

Coverage is deliberate: an event at each step of the transfer lifecycle, including the moment a deposit lands, and an event at each step of customer verification.

What ships today

Transfers were previously observable only by calling GET /v2/transfer/{id} on a schedule. There are now 14 transfer events spanning the full state machine: transfer.created, transfer.fundsReceived, transfer.processing, and transfer.succeeded on the happy path, and a distinct event for every failure, return, and refund outcome, including transfer.failed, transfer.returned, transfer.refunded, and transfer.expired.

Four events cover customer verification. customer.created fires once when the record is created. customer.pending, customer.approved, and customer.rejected fire once per verification profile per transition, so a customer carrying two profiles produces two independent event streams.

You subscribe per endpoint, either to specific event names or to a wildcard like transfer.*.

Why the payload is small

There is a design decision worth explaining here, because it shapes how you will write your handler.

A webhook body could carry the entire resource. We chose not to do that, and the choice shapes how you write your handler. Each event carries the resource ID, the previous and new status with timestamps, the relational IDs needed to match the event to your own records, and a structured cause when something failed:

{
 "id": "event_01HXP9ZK7Q4ABC123",
 "type": "transfer.succeeded",
 "apiVersion": "v2",
 "livemode": true,
 "sequence": 5,
 "data": {
   "id": "payout_d243ab2b1de4447d8a046d87fefe58cf",
   "type": "transfer",
   "customerId": "customer_f31121c389624d3697cbf3ea8830b7a4",
   "applicationId": "application_26b980e989d94e6d975144f7e21f282f",
   "transferType": "oneTimeTransfer",
   "previousStatus": "processing",
   "previousStatusAt": "2026-04-23T14:04:30.000Z",
   "status": "succeeded",
   "statusAt": "2026-04-23T14:05:30.000Z",
   "cause": null
 }
}


The reasoning is that most handlers do not need the full object. Updating a row or notifying an operations channel works from the body alone. A fat payload would mean shipping fields you never read on every delivery. When your logic does need the complete resource, you call GET on it. We would rather you make that call deliberately than pay for it on every event.

Failures tell you why

Every terminal failure event carries a structured cause. A rejected customer arrives with a value like identity_verification_failure, document_mismatch, incomplete_information, or restricted_jurisdiction. A failed transfer arrives with insufficient_funds, recipient_bank_rejection, invalid_recipient, or quote_expired.

These are enum values rather than prose. You can branch on them androute on them without writing a parser against strings we might reword later.

Ordering is one integer

Networks reorder things, and a webhook system that ignores this hands the problem to you.

Every event carries a sequence: a monotonically increasing integer scoped to the individual resource and to what your endpoint subscribes to. Events of types you did not subscribe to do not increment it, so you will never see a gap and wonder what you missed.

The reconciliation pattern is one comparison against a column on a row you already maintain:

if (event.sequence <= customer.lastSphereSequence) return ack();

No global counter, no ordering service, no queue of your own. You may occasionally receive the same event more than once; the event id is stable across every delivery of the same event and is your idempotency key.

Verifying a delivery

Each endpoint has its own signing secret, returned once when you create the endpoint and never again. Every delivery is signed with HMAC-SHA256 over the timestamp and the raw body:

const expected = crypto.createHmac("sha256", secret)
 .update(`${headers["sphere-timestamp"]}.${rawBody}`)
 .digest("hex");

Compare that against the v1= scheme in the Sphere-Signature header, and reject anything whose Sphere-Timestamp is more than five minutes old. Verify against the raw bytes rather than a re-serialized body. A JSON round trip will change them and the signature will not match.

Return a 2xx within fifteen seconds and we consider the event delivered.

Copy-paste verification implementations for TypeScript, Python, and Go are in the docs.

Managing endpoints

Register an endpoint with POST /v2/webhook-endpoints, listing the events you want. Each account can hold up to six.

GET /v2/events returns what we sent you. Filter by endpoint, event type, delivery status, or date range, and open any event to see each delivery attempt with its response code and latency. This is the same data that will back the dashboard, exposed as an API first so you can build reconciliation and alerting against it directly.

Already using webhooks?

Your existing endpoints keep working. Webhooks v2 is a separate, versioned surface. Nothing switches on underneath your existing endpoints. Createa v2 endpoint through the API, validate it against your handler, and retire the old one when you are ready. Existing endpoints will be supported for at least twelve months, and we will give clear notice well before that changes. The migration steps are in the docs.

What is coming next

A webhooks dashboard is close behind this release. It will give you the event log as a browsable interface, a button to fire test events against your endpoint before you go live, and one-click replay for any delivery that did not land. Replay arrives with it.

After that: automatic retries and endpoint health monitoring, so a brief outage on your side recovers without anyone intervening. Then in-place endpoint updates, secret rotation, a replay API, and official SDKs for TypeScript, Python, and Go. Document and service events will follow.

Get started here
Questions? Reach out to support@spherepay.co.

Subscribe to Sphere Blog

No spam. Just the latest releases and tips, interesting articles, and exclusive interviews in your inbox every week.

Construyamos el futuro de las finanzas, más rápido

Únase a las empresas que ya están creciendo con Sphere.

Empezar
Lea los documentos