5. Webhooks
5. Webhooks
Webhooks
WhenWebhooks eventslet happenSIR (Giving notify your backend when something changes. For example, you can receive an event when an action completes, aan campaignaction starts,fails, a token pool runs low),low, we POSTor a JSONcampaign payloadchanges status.
Use webhooks when your system needs a durable server-side record of reward outcomes.
How webhook delivery works
RegisteringRegister a webhook
POST /v1/partner/webhooks
(sk_X-Partner-Key: keysk_test_...
required)X-Timestamp: <timestamp>
X-Signature: <signature>
Content-Type: application/json
{
"url": "https://your-app.example.com/webhooks/sir",
"description": "ProductionSandbox webhook",
"eventTypes": ["action.completed", "action.failed", "transaction.completed"],
"receiveAllEvents": false,
"headers": { "X-My-Tenant": "prod" }false
}
Response (the secret is shown once — store it):Response:
{
"id": "..."webhook-id",
"url": "https://your-app.example.com/webhooks/sir",
"eventTypes": [..."action.completed", "action.failed"],
"secret": "whsec_<64hex>whsec_...",
"isActive": true,
...true
}
Store receiveAllEvents: truesecretsubscribesimmediately. youIt tois everyshown event type. Otherwise, only the eventTypes[] you list.once.
Delivery formatheaders
We POST application/json with these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
SIRGiving-Webhooks/1.0 |
X-SIR-Signature |
sha256=<hex> |
X-SIR-Timestamp |
Payload
Event envelope
{
"id": "evt_abc123",
"type": "action.completed",
"createdAt": "2026-05-10T09:12:00Z",
"data": {
/*"actionId": event-specific"...",
shape"tokensDistributed": */50
}
}
SignatureVerify verificationsignatures
Algorithm:SIR Giving signs the timestamp and raw request body:
signedPayload = `${X-SIR-Timestamp}Timestamp + ".${rawRequestBody}`" + rawRequestBody
expected = `"sha256=`" + HMAC-SHA256_hex(HMAC_SHA256_hex(webhookSecret, signedPayload)
verify = constant-time-equal(X-SIR-Signature, expected)
TheCompare bodyexpected isto signedX-SIR-Signature asusing theconstant-time exact bytes we sent — do not re-serialize before hashing.comparison.
Node.js verifier (Express raw body)
example
import crypto from 'crypto';
import express from 'express';
const app = express();
app.use('/webhooks/sir', express.raw({ type: 'application/json' }));
// raw body!
app.post('/webhooks/sir', (req, res) => {
const sigsignature = req.header('X-SIR-Signature') ?? '';
const tstimestamp = req.header('X-SIR-Timestamp') ?? '';
const rawBody = req.body as Buffer;
// Buffer because of express.raw
const signedPayload = `${ts}timestamp}.${rawBody.toString()}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET!SIR_WEBHOOK_SECRET!)
.update(signedPayload)
.digest('hex');
const okvalid =
sig.signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig)signature), Buffer.from(expected));
if (!ok)valid) return res.status(401).end();
//const Optional:ageSeconds reject= old events to prevent replay
if (Math.abs(Date.now() / 1000 - Number(ts)timestamp));
if (ageSeconds > 300) return res.status(401).end();
const event = JSON.parse(rawBody.toString());
// …Store handor offenqueue tothe aevent queue andhere.
return quickly res.status(200).end();
});
Python verifier (Flask)
import hmac, hashlib, time, os
from flask import request, abort
@app.post("/webhooks/sir")
def hook():
sig = request.headers.get("X-SIR-Signature", "")
ts = request.headers.get("X-SIR-Timestamp", "")
raw = request.get_data()
signed_payload = f"{ts}.{raw.decode()}"
expected = "sha256=" + hmac.new(
os.environ["WEBHOOK_SECRET"].encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
if abs(time.time() - int(ts)) > 300:
abort(401)
event = request.get_json()
# … enqueue, return 200
return "", 200
Retry & failure behavior
We expectReturn a 2xxwithinresponseawhenreasonableyoutimeout.accept the event.
statusGET PENDINGRETRYINGDELIVEREDFAILED/v1/partner/webhooks/:id/deliveries.disabledReasonPOST /v1/partner/webhooks/:id/deliveries/:deliveryId/retry.
Test your webhook
POST /v1/partner/webhooks/:id/test
X-Partner-Key: sk_test_...
X-Timestamp: <timestamp>
X-Signature: <signature>
Then check:
GET /v1/partner/webhooks/:id/deliveriesreturns a paginated, filterable history including request payload, response status/body (truncated to 10 KB), and timing.
EventCommon event types
Actions
action.completed |
An action processed successfully |
action.failed |
Action processing failed |
action.reversed |
An action was fully or partially reversed |
Transactions
transaction.completed
Token distribution transaction transaction.reversed
Token distribution transaction Redemptions
redemption.approvedredemption.rejectedredemption.completedToken pools
token_pool.low_balance
Pool dropped below threshold
token_pool.depleted
Pool token_pool.refilled
Pool was topped up
token_pool_request.approved
Allocation request approved
token_pool_request.rejected
Allocation request rejected
Users
user.createduser.upgradedCampaigns
campaign.activated
Campaign moved to campaign.paused
Campaign paused
campaign.completed
Campaign ended
campaign.budget_reachedBulk
bulk_job.startedbulk_job.completedbulk_job.failedPartner / API key
partner.status_changedapi_key.expiring
One of your keys is expiresAtexpiry
Testing
POST /v1/partner/webhooks/:id/test dispatches a partner.status_changed ping with body { "type": "ping", "message": "Test webhook delivery from Partner API", "webhookId": "...", "timestamp": "..." }. Use this to verify your endpoint is reachable and your signature verification is correct.
The GET /v1/partner/dashboard/webhooks/health endpoint returns the last-24h success rate, average response time, and recent failures across all your webhooks.