Skip to main content

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

    You register an HTTPS endpoint. SIR Giving returns a webhook signing secret once. SIR Giving sends events to theyour URLendpoint. youYour registered.server You verifyverifies the signature,signature. returnYour 2xx,server andstores processor asynchronously.queues the event. Your server returns a 2xx response quickly.

    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: truesecret subscribesimmediately. 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 unixUnix timestamp in seconds
    (any custom headers you set on the webhook)

    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 2xx withinresponse awhen reasonableyou timeout.accept the event.
    Anything else countsis treated as a failure.failed delivery. Failed deliveries are retried with exponentialbackoff. backoff.Slow Thehandlers can cause duplicate deliveries, so return quickly and process asynchronously. You can inspect delivery record'shistory with statusGET cycles through PENDING → RETRYING → DELIVERED or FAILED/v1/partner/webhooks/:id/deliveries. After N consecutive failures across all deliveries, the webhook is auto-disabled and disabledReason is set. You'll need to register a new endpoint or contact support to re-enable. You can manually re-queueretry a singlefailed failure: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: <timestamp>
    X-Signature: <signature>
    

    Then check:

    GET /v1/partner/webhooks/:id/deliveries
     returns a paginated, filterable history including request payload, response status/body (truncated to 10 KB), and timing.
    

    EventCommon event types

    Actions

    TypeEvent FiresWhen whenit fires
    action.completed An action processed successfully and tokens were distributed
    action.failed Action processing failed
    action.reversed An action was fully or partially reversed

    Transactions

    Type transaction.completed Token distribution transaction succeededcompleted transaction.reversed Token distribution transaction was reversed

    Redemptions

    Type redemption.approved Redemption request approved redemption.rejected Redemption request rejected redemption.completed Redemption fulfilled

    Token pools

    Type token_pool.low_balance Pool dropped below threshold token_pool.depleted Pool emptyhas no remaining balance token_pool.refilled Pool was topped up token_pool_request.approved Allocation request approved token_pool_request.rejected Allocation request rejected

    Users

    Type user.created New partner user created user.upgraded Partner user upgraded to full SIR account

    Campaigns

    Type campaign.activated Campaign moved to ACTIVEactive campaign.paused Campaign paused campaign.completed Campaign ended campaign.budget_reached Campaign budget hit

    Bulk

    Type bulk_job.started Bulk processing started bulk_job.completed Bulk processing finished bulk_job.failed Bulk processing failed

    Partner / API key

    Type partner.status_changed Your partner status changed (also used for the test ping) api_key.expiring One of your keys is approachingnearing its 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.