The events
Subscribing
POST /partner/v1/webhooks:
secret — it’s shown exactly once. This is what you’ll use to verify every incoming webhook’s signature.
The payload shape
Every webhook delivery is aPOST with this shape:
Verifying the signature
Same shape as Stripe and similar. The signature header has two fields:t— the Unix timestamp when we signed.v1— the HMAC-SHA256 of{timestamp}.{raw_body}using your webhook secret.
1
Extract t and v1 from the header
Split on
, and parse the key=value pairs.2
Check the timestamp
Reject if
t is more than 5 minutes off from your server’s time (replay protection).3
Compute the expected signature
HMAC_SHA256(secret, "{t}." + raw_body) and hex-encode.4
Compare with v1 in constant time
Use a constant-time comparison (
hmac.compare_digest in Python, crypto.timingSafeEqual in Node, hash_equals in PHP, CryptographicOperations.FixedTimeEquals in C#, MessageDigest.isEqual in Java). Do not use string equality.5
Process the event only if signature matches
Otherwise return 401 and log the event for investigation.
Reference implementations
Copy-paste-ready verifiers in five languages (JavaScript, Python, PHP, C#, Java) live on Security → Webhook verification → Verifying in your language. Each one handles signature parsing, timestamp drift checking, HMAC computation, and constant-time comparison. Use those rather than implementing from scratch. Reference implementations in Python, Node.js, and Go on the Partner Portal.Idempotency
Webhooks can be redelivered. Treat them as at-least-once. Every event has anid (e.g., evt_01HMFB...). Store the ID after successful processing. On the next delivery, check the store — if you’ve seen the ID, return 200 immediately without reprocessing.
A simple Redis or Postgres events_processed(event_id PRIMARY KEY, received_at TIMESTAMP) table works for almost everyone. TTL the rows at 30 days.
Looking up an event
You can look up any event your organisation has been delivered, by ID.partnerId).
Response 200
string
Event ULID prefixed
evt_.string
Event type, e.g.
DISBURSEMENT_DELIVERED.object
The exact JSON body we POSTed to your endpoint(s).
string
ISO-8601 timestamp the event was raised.
array
One entry per webhook subscription that received this event. Each delivery has the same shape as
GET /webhooks/:id/deliveries: id, webhookId, status (PENDING|DELIVERED|FAILED|GIVEN_UP), attempt, lastStatusCode, firstAttemptAt, nextAttemptAt, deliveredAt, givenUpAt, createdAt, lastResponseBody.Error cases
Use this to reconcile audit-log discrepancies, debug a missed delivery, or correlate a signature you logged with the event we generated.
Delivery and retries
We deliver from a queue. Order is not guaranteed — events for the same user may arrive out of order. Use thecreatedAt field on the event payload if order matters for your processing.
On a non-2xx response (or no response within 30 seconds), we retry with exponential backoff:
After 14 days of failed delivery, we mark the event as undeliverable and stop retrying. You can replay it from the Partner Portal dashboard later.
We don’t count 200, 201, 202, 204 as success — any 2xx works. We treat 410 Gone as “permanently disabled — don’t retry.”
Filtering
You can configure multiple webhook subscriptions, each filtered to a subset of events. Common patterns:- One subscription for consent events (
consent.*) routed to your customer-success system. - One subscription for standing changes routed to your underwriting service.
- One subscription for repayment events routed to your loan-servicing system.
Listing and managing subscriptions
Local development
Three good ways to receive webhooks without standing up a backend:- webhook.site — instant temporary URL that captures and displays every request it receives, including our headers and the signed body. Best for a quick smoke test.
- smee.io — forwards public webhook traffic to a local URL on your machine.
- ngrok — public tunnel to your localhost service.
Testing from the Partner Portal
Inside the Partner Portal atpartners.guild.com.ng/webhooks, every active subscription has a Send test action. It fires a synthetic delivery through the same queue and signing pipeline as real events — the only difference is a test: true flag in the envelope so your handler can filter it out if it wants to.
The test delivery picks an event your subscription is registered for, generates a representative sample payload, signs it with your webhook secret, and posts it to your URL. It then appears in the deliveries panel with timestamps, response code, and retry status, exactly like a real event.
You can also replay any prior delivery from the Portal — useful for debugging a handler crash without waiting for the next live trigger.
Webhook security checklist
The standard list. We won’t enforce all of these but you’ll thank yourself for them later.- ✅ Verify signatures on every request. Reject if the signature fails.
- ✅ Reject events older than 5 minutes (replay protection).
- ✅ Check idempotency using
event_id. - ✅ Return 2xx quickly. Don’t do heavy processing inline — push to a queue.
- ✅ Run handlers in a separate worker if the work is heavy.
- ✅ Log failed deliveries with the request body for debugging.
- ✅ Keep the webhook URL stable. If you must change it, update the subscription before changing your DNS.
- ✅ Monitor delivery success rate. We surface it in the dashboard; alert if it drops below 99%.
Failure modes you’ll see
Next
Rate limits
Throughput numbers for the API.
Snapshots
The data that lives behind every consent.