Agent skill
Stitch Webhooks Skill
Receive and verify Stitch (stitch.money) payment webhooks delivered via Svix. Use when setting up a Stitch webhook handler, debugging svix-id / svix-timestamp / svix-signature verification, or handling Stitch payment, payment.confirmation, transaction, payment-consent-request, collection, refund, and disbursement status events.
Install this skill
npx skills add hookdeck/webhook-skills --skill stitch-webhooks
Stitch (stitch.money, the South African payments API, not Stitch Data the ETL tool) delivers webhooks through Svix. Each webhook subscription has its own signing secret (whsec_...), returned when you create the subscription via the GraphQL API or shown in the Svix consumer portal.
When to Use This Skill
- How do I receive Stitch webhooks?
- How do I verify Stitch webhook signatures (
svix-signature)? - How do I handle Stitch
payment,refund, ordisbursementstatus updates? - Why is my Stitch webhook signature verification failing?
- How do I subscribe to Stitch webhooks with
clientWebhookAdd? - How do I migrate from Stitch legacy webhooks to Svix webhooks?
Verification (core)
Every delivery carries three headers:
svix-id: msg_2b1c... # unique message id (use for idempotency)
svix-timestamp: 1614265330 # Unix seconds
svix-signature: v1,g0hM9SsE... v2,... # space-delimited "version,signature" entries
The signed content is ${svix-id}.${svix-timestamp}.${raw_, signed with HMAC-SHA256 using the base64-decoded bytes of the secret after the whsec_ prefix, base64-encoded. Compare only the v1, entries. Stitch recommends the official svix library (Node and Python), which also handles the 5-minute timestamp tolerance, rotation, and constant-time comparison:
import { Webhook } from 'svix'; // svix 2.x is ESM-only
const wh = new Webhook(process.env.STITCH_WEBHOOK_SECRET); // "whsec_..."
wh.verify(rawBody, { // rawBody: raw Buffer/string, NOT parsed JSON
'svix-id': req.headers['svix-id'],
'svix-timestamp': req.headers['svix-timestamp'],
'svix-signature': req.headers['svix-signature'],
}); // throws WebhookVerificationError on bad signature / stale timestamp
const event = JSON.parse(rawBody.toString('utf8')); // parse only after it passes
Python is the same: from svix.webhooks import Webhook and Webhook(secret).verify(raw_. For a manual (no SDK) implementation, see references/verification.md.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
You subscribe with filterTypes on clientWebhookAdd (or in the Svix portal):
| Event type | Product | Sent when |
|---|---|---|
payment | Pay by Bank, Capitec Pay, Card (once-off), Manual EFT, Cash | Payment request status changes (Completed, Cancelled, Expired) |
payment.confirmation | Same, with a Stitch intermediary account | Funds received (PaymentReceived, PaymentUnsettled) |
payment-consent-request | Card, Capitec Pay Recurring, DebiCheck | Consent status changes (GRANTED, FAILED, REVOKED, ...) |
transaction | Card, Capitec Pay Recurring, DebiCheck | Transaction reaches SUCCESS, FAILURE, CANCELLED |
collection | Batch collections (subscriptions) | A collection reaches completed, failed, cancelled |
refund | Refunds | Refund status changes (Submitted, Completed, Error, Paused) |
disbursement | Disbursements | Disbursement status changes (Submitted, Completed, Error, Paused, Cancelled, Reversed) |
Payload gotcha: payment-consent-request, transaction, and collection payloads have a top-level type field. payment, payment.confirmation, refund, and disbursement payloads are GraphQL-shaped ({"data": {"client": {"paymentInitiationRequests" | "refunds" | "disbursements": {"node": {...}}}}}) with no type field, so dispatch on the data.client key. payment and payment.confirmation share the same shape; read state.__typename and paymentConfirmation._ on the node. The examples include a getEventType() helper that handles both shapes.
Deprecated types (
direct-deposit,payment-initiation,payment-initiation.confirmation) still exist; see references/overview.md.
Environment Variables
# Svix signing secret for your Stitch webhook subscription (starts with whsec_)
STITCH_WEBHOOK_SECRET=whsec_xxxxx
Local Development
For local webhook testing, run the Hookdeck CLI via npx (no install required):
npx hookdeck-cli listen 3000 stitch --path /webhooks/stitch
No account required. Register the printed URL as your subscription url.
Reference Materials
- references/overview.md - Event types, statuses, payload shapes
- references/setup.md - Creating a subscription (GraphQL API or Svix portal), getting the secret
- references/verification.md - SDK and manual verification, gotchas, debugging