5. Webhooks Webhooks let SIR Giving notify your backend when something changes. For example, you can receive an event when an action completes, an action fails, a token pool runs low, or a campaign changes status. Use webhooks when your system needs a durable server-side record of reward outcomes. How webhook delivery works You register an HTTPS endpoint. SIR Giving returns a webhook signing secret once. SIR Giving sends events to your endpoint. Your server verifies the signature. Your server stores or queues the event. Your server returns a 2xx response quickly. Register a webhook POST /v1/partner/webhooks X-Partner-Key: sk_test_... X-Timestamp: X-Signature: Content-Type: application/json { "url": "https://your-app.example.com/webhooks/sir", "description": "Sandbox webhook", "eventTypes": ["action.completed", "action.failed"], "receiveAllEvents": false } Response: { "id": "webhook-id", "url": "https://your-app.example.com/webhooks/sir", "eventTypes": ["action.completed", "action.failed"], "secret": "whsec_...", "isActive": true } Store secret immediately. It is shown once. Delivery headers Header Value Content-Type application/json User-Agent SIRGiving-Webhooks/1.0 X-SIR-Signature sha256= X-SIR-Timestamp Unix timestamp in seconds Event envelope { "id": "evt_abc123", "type": "action.completed", "createdAt": "2026-05-10T09:12:00Z", "data": { "actionId": "...", "tokensDistributed": 50 } } Verify signatures SIR Giving signs the timestamp and raw request body: signedPayload = X-SIR-Timestamp + "." + rawRequestBody expected = "sha256=" + HMAC_SHA256_hex(webhookSecret, signedPayload) Compare expected to X-SIR-Signature using constant-time comparison. Node.js Express example import crypto from 'crypto'; import express from 'express'; const app = express(); app.use('/webhooks/sir', express.raw({ type: 'application/json' })); app.post('/webhooks/sir', (req, res) => { const signature = req.header('X-SIR-Signature') ?? ''; const timestamp = req.header('X-SIR-Timestamp') ?? ''; const rawBody = req.body as Buffer; const signedPayload = `${timestamp}.${rawBody.toString()}`; const expected = 'sha256=' + crypto .createHmac('sha256', process.env.SIR_WEBHOOK_SECRET!) .update(signedPayload) .digest('hex'); const valid = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!valid) return res.status(401).end(); const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp)); if (ageSeconds > 300) return res.status(401).end(); const event = JSON.parse(rawBody.toString()); // Store or enqueue the event here. return res.status(200).end(); }); Retry behavior Return a 2xx response when you accept the event. Anything else is treated as a failed delivery. Failed deliveries are retried with backoff. Slow handlers can cause duplicate deliveries, so return quickly and process asynchronously. You can inspect delivery history with GET /v1/partner/webhooks/:id/deliveries . You can manually retry a failed delivery with POST /v1/partner/webhooks/:id/deliveries/:deliveryId/retry . Test your webhook POST /v1/partner/webhooks/:id/test X-Partner-Key: sk_test_... X-Timestamp: X-Signature: Then check: GET /v1/partner/webhooks/:id/deliveries Common event types Event When it fires action.completed An action processed successfully action.failed Action processing failed action.reversed An action was fully or partially reversed transaction.completed Token distribution transaction completed transaction.reversed Token distribution transaction reversed token_pool.low_balance Pool dropped below threshold token_pool.depleted Pool has no remaining balance token_pool.refilled Pool was topped up token_pool_request.approved Allocation request approved token_pool_request.rejected Allocation request rejected campaign.activated Campaign moved to active campaign.paused Campaign paused campaign.completed Campaign ended api_key.expiring One of your keys is nearing expiry