Browse docs

Developers

Webhooks and events

Signed webhook deliveries: registering an endpoint, verifying the x-wc-signature header, the retry schedule, and every event type Soca emits with what to do when it fires.

Webhooks are how Soca tells your backend that money moved. Register an HTTPS endpoint in Dashboard → Developers → Webhooks and you receive a signing secret starting with whsec_. From then on, every event is POSTed to your endpoint as JSON:

Delivery body
{
"id": "evt_...",
"type": "invoice.paid",
"created_at": "2026-07-21T09:30:00.000Z",
"data": { ... }
}
  • x-wc-signature: t=<unix-seconds>,v1=<hex hmac>: the signature to verify (below).
  • wc-event-type: <event type>: the event type, duplicated as a header for cheap routing.

Verifying signatures

The signature is HMAC-SHA256 over "<t>.<raw request body>" with your whsec_ secret, compared timing-safe against v1. Reject deliveries whose t is more than 5 minutes old; that is the replay window. Always verify against the raw body, before any JSON parsing or re-serialization.

With the SDK it is one call:

import { verifyWebhookSignature } from '@usesoca/sdk';
const ok = verifyWebhookSignature(process.env.SOCA_WEBHOOK_SECRET, rawBody, req.headers['x-wc-signature']);
if (!ok) return res.status(400).end();

Without the SDK, the same check in plain Node:

Manual verification
import crypto from 'node:crypto';
export function verifyWebhook(secret, rawBody, header) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1 || '', 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Retries

Respond with any 2xx status to acknowledge a delivery. On a non-2xx response or a network error, Soca retries with backoff. The next attempts come 1, 5, 30, and 120 minutes after each failure, for 5 attempts in total. The delivery is then marked failed and recorded for inspection. Deliveries can arrive late or, rarely, more than once, so key your fulfillment on the event id and make duplicates a no-op.

Event types

Payments and subscriptions emit exactly these events:

EventWhen it firesWhat to do
invoice.paidA one-time invoice was paid.Fulfill the order. This is the fulfillment signal for one-time payments.
refund.succeededA refund completed on-chain.Reverse the fulfillment or record the credit in your books.
subscription.activeThe first successful subscription charge.Start access. This is the fulfillment signal for subscriptions.
subscription.renewedA renewal charge succeeded.Extend access for another period; usually nothing else.
subscription.payment_failedA renewal charge failed. Dunning (automatic retries) begins.Optionally notify the customer to top up their wallet. Do not revoke access yet.
subscription.dunning_exhaustedAll renewal retries failed; the subscription is lost.End access and treat the subscription as churned.
subscription.revokedThe customer revoked their on-chain authorization.End access immediately; no further charges are possible.
subscription.expiredThe subscription reached its scheduled end.End access.

Usage-billing actions taken through the API append to the same event feed and ride the same signed, retried delivery path:

EventWhen it fires
credit.grantedA credit grant was applied via POST /v1/credit_grants.
credit.debitedCredits were drawn down via POST /v1/credit_debits.
entitlement.grantedA feature was granted via POST /v1/entitlement_grants.

There is no payment.confirmed event

Do not wait for one. Fulfill one-time purchases on invoice.paid and start subscription access on subscription.active; revoke on subscription.revoked, subscription.dunning_exhausted, or subscription.expired.

Reconciliation

The same events are queryable at `GET /v1/events`. If your endpoint was down past the retry window, page through the feed and process anything you missed. Webhooks are the push; the events list is the pull.

Was this page helpful?

A quick signal helps us improve the right pages.