SpareSpare Docs
GuidesAPI Reference

Webhook Event Format

The HTTP request Spare sends to your callback URL, with decoded examples for each resource type.

When an event fires, Spare sends one POST request to each active subscription URL that matches the resource and product. The body is a compact JWT signed with ES256. Your server must return 202 Accepted. Any other status triggers a retry.

HTTP request

POST https://your-server.example.com/webhooks/spare
content-type: application/jose
x-spare-webhook-id: evt_01JZZQ8K5M2N3P4Q5R6S7T8U9V
x-spare-webhook-delivery-id: del_01JZZQ8K5M2N3P4Q5R6S7T8UAW
x-spare-webhook-attempt: 1
x-spare-webhook-version: v1.0

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ijg3MGQ2ZmM4In0.eyJpc3MiOiJodHRwczovL2FwaS50cnlzcGFyZS5hZSIsInN1YiI6IndlYmhvb2s6ZXZlbnQiLCJhdWQiOiJjbGlfMDFKWlpRMTIzNDU2Nzg5MCIsImp0aSI6ImV2dF8wMUpaWlE4SzVNMk4zUDRRNVI2UzdUOFU5ViIsImlhdCI6MTc1NzM0ODc1NCwiZXhwIjoxNzU3MzQ5MDU0LCJldmVudCI6e319.signature
HeaderDescription
content-typeAlways application/jose. The body is a compact JWT, not JSON.
x-spare-webhook-idStable event ID (evt_...). Identical on every retry of the same event. Use this to deduplicate.
x-spare-webhook-delivery-idDelivery attempt ID (del_...). Changes on each retry.
x-spare-webhook-attemptAttempt counter, starting at 1.
x-spare-webhook-versionSchema version, e.g. v1.0.

JWT structure

Split the body on . to get three base64url segments: header, payload, signature.

Header:

{
  "alg": "ES256",
  "typ": "JWT",
  "kid": "870d6fc8..."
}

Payload:

{
  "iss": "https://api.tryspare.ae",
  "sub": "webhook:event",
  "aud": "cli_01JZZQ1234567890",
  "jti": "evt_01JZZQ8K5M2N3P4Q5R6S7T8U9V",
  "iat": 1757348754,
  "exp": 1757349054,
  "event": { }
}
ClaimDescription
issSpare issuer URL. Match this against the known production or sandbox host.
subAlways webhook:event.
audYour client ID. Verify it matches the credential that owns the subscription.
jtiSame as x-spare-webhook-id. Use either for deduplication.
iat / expIssued-at and expiry seconds (UTC). The JWT expires 5 minutes after issue.
eventThe outbound event envelope. See below.

Verifying the signature

Fetch the signing key set from the JWKS endpoint:

GET https://api.sandbox.tryspare.ae/webhook/jwks

The response is a standard JWK Set ({ "keys": [...] }). Use the kid in the JWT header to pick the matching key, then verify the ES256 signature with any JWT library before trusting the payload.

import * as jose from 'jose';

const JWKS_URL = 'https://api.sandbox.tryspare.ae/webhook/jwks';
const jwks = jose.createRemoteJWKSet(new URL(JWKS_URL));

async function verifySpareWebhook(rawBody: string, clientId: string) {
  const { payload } = await jose.jwtVerify(rawBody, jwks, {
    algorithms: ['ES256'],
    audience: clientId,
    subject: 'webhook:event',
  });
  return payload.event; // the outbound envelope
}

Event envelope fields

The event claim carries the outbound envelope:

FieldTypeDescription
idstringUnique event ID (evt_...). Stable across retries.
typestringPublic event type, e.g. payment_request.completed.
versionstringSchema version, e.g. v1.0.
occurredAtstringISO 8601 timestamp with +00:00 offset.
productstringProduct that emitted the event, e.g. payments.
tenantstringTenant, e.g. UAE.
clientIdstringYour client ID.
resource.typestringResource type: payment_request, consent, payment, mandate, mandate_transaction.
resource.idstringID of the resource that changed.
correlationobjectIDs linking related resources. Always includes paymentRequestId.
data.statusstringPlatform status at the time of the event.
data.previousStatusstringPrevious platform status, when available.

Sample envelopes by resource type

Event is a notification, not a source of truth

The event carries a status snapshot at delivery time. For authoritative state, fetch the resource directly after receiving the event: for example, GET /payment-requests/{paymentRequestId} after a payment_request.* event.

On this page