Skip to main content

6. End-to-End Scenarios

End-to-End Scenarios

Worked examples covering the most common partner integrations. Each scenario shows API calls in order, the data flowing between them, and which webhooks to expect.


Scenario 1 — Reward a user when your customer makes a purchase

Goal: A retail customer of yours buys a product. You want to credit them with SIR tokens proportional to their spend (a "donate-with-purchase" or loyalty pattern).

Flow

[Customer checks out]              ─── your store ───┐
                                                     │
[Your backend: order.completed]                      │
                                                     │
[POST /v1/partner/actions/submit] ───── SIR API ─────┘
       │
       ├─ token pool debited
       ├─ user account credited
       └─ webhook: action.completed → your endpoint

Request

POST /v1/partner/actions/submit
X-Partner-Key: sk_live_...
X-Timestamp: 1746876000
X-Signature: <hmac>
Content-Type: application/json

{
  "idempotencyKey": "order_98765",
  "actionType": "PURCHASE",
  "amount": 49.99,
  "currency": "USD",
  "stakeholders": [
    {
      "stakeholderTypeCode": "CUSTOMER",
      "partnerUserId": "user_42",
      "userEmail": "customer@example.com",
      "userFirstName": "Jane",
      "userLastName": "Doe"
    }
  ],
  "autoCreateUsers": true,
  "metadata": {
    "orderId": "98765",
    "sku": "WIDGET-001",
    "channel": "web"
  }
}

Response (200)

{
  "actionId": "65f1...",
  "idempotencyKey": "order_98765",
  "status": "COMPLETED",
  "tokensDistributed": 50,
  "transactionIds": ["65f2..."]
}

What to do next

  • Show the user a "+50 SIR" toast (or whatever your campaign multiplier yields).
  • Persist actionId against your order so you can later reverse it on a refund.
  • Listen for action.completed if you want server-side confirmation rather than trusting the response.

Failure modes

  • 402 INSUFFICIENT_POOL — your token pool is empty. Resolve by POST /v1/partner/token-pools/request for more allocation.
  • 409 IDEMPOTENT — same idempotencyKey was already processed; you get the original response back. Safe to retry.
  • 429 — rate-limited. Honor Retry-After.

Scenario 2 — Customer-initiated donation widget

Goal: Show a "Donate to Red Cross" button on your site. Clicking redirects to a hosted Stripe / Every.org checkout. After completion, the donor earns SIR tokens automatically.

Flow

[Browser]                                            [Your backend]                  [SIR API]
   │ widget mounts with pk_live_...                     │                                │
   │ GET /v1/partner/config         ─────────────────────────────────────────────────────▶│
   │ ◀─────────── feature flags / tip config ───────────────────────────────────────────│
   │ user clicks "Donate $25 to Red Cross"               │                                │
   │ POST /v1/partner/donations/create-link             ─────────────────────────────────▶│
   │ ◀───── { donationLink, partnerDonationId } ────────────────────────────────────────│
   │ window.location = donationLink                      │                                │
   │   [Stripe / Every.org checkout]                     │                                │
   │   …user pays…                                       │                                │
   │                                          (Stripe webhook → SIR; SIR processes      │
   │                                           donation, distributes tokens)             │
   │                                                     │                                │
   │  [Optional poll]                                    │                                │
   │  GET /v1/partner/donations/status/:partnerDonationId────────────────────────────────▶│
   │  ◀── { status: "COMPLETED", tokensDistributed: 175 } ──────────────────────────────│
   │                                                     │                                │
   │                            POST your webhook URL ──────── action.completed ────────│

Browser code

const r = await fetch('https://api.sirgiving.org/v1/partner/donations/create-link', {
  method: 'POST',
  headers: {
    'X-Partner-Key': 'pk_live_...',           // publishable key, safe in browser
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    nonprofitSlug: 'american-red-cross',
    amount: 2500,                              // $25.00 in cents
    currency: 'USD',
    frequency: 'once',
    provider: 'stripe',
    donorEmail: 'jane@example.com',
    firstName: 'Jane',
    lastName: 'Doe',
  }),
});
const { donationLink, partnerDonationId } = await r.json();
window.location.href = donationLink;

What to do next

  • Save partnerDonationId against the user session to show a "Thanks for donating!" page on return.
  • Subscribe to action.completed and action.failed server-side to definitively know when tokens land.

Scenario 3 — Reverse a reward when an order is refunded

Goal: A customer returns the widget you rewarded them for in Scenario 1. Pull the SIR tokens back.

Flow

[Refund initiated]
     │
     ▼
[POST /v1/partner/actions/{actionId}/reverse]
     │
     ├─ tokens clawed back from user wallet
     ├─ if user already spent some, a "debt" is created
     └─ webhook: action.reversed

Request

POST /v1/partner/actions/65f1.../reverse
X-Partner-Key: sk_live_...        # MUST be sk; pk rejected
…HMAC headers…

{
  "reversalPercentage": 100,
  "reason": "Order refunded - return processed",
  "refundIdempotencyKey": "refund_order_98765"
}

Required scope: the API key must have actions:reverse in its scope list. Otherwise → 403 INSUFFICIENT_SCOPE. Ask your admin contact to attach the scope when generating the key.

Response

{
  "reversalTransactionId": "...",
  "tokensClawedBack": 35,
  "debtsCreated": 1,
  "breakdown": [
    { "stakeholderTypeCode": "CUSTOMER", "expected": 50, "clawedBack": 35, "debt": 15 }
  ]
}

debtsCreated > 0 means the user had already redeemed some of the rewarded tokens. The debt is tracked on their account and will offset future earnings.


Scenario 4 — Run a 2X holiday campaign

Goal: Double SIR rewards for all donations between Black Friday and New Year's.

Steps

  1. Pick a token pool to fund the campaign: GET /v1/partner/token-pools → identify the pool with enough headroom.
  2. Create the campaign in DRAFT:
    POST /v1/partner/campaigns
    {
      "tokenPoolId": "...",
      "name": "Holiday 2X",
      "templateId": "REWARD_MULTIPLIER",
      "actionTypes": ["DONATION"],
      "distribution": { "baseMultiplier": 2.0, "stakeholderSplits": [...] },
      "limits": { "totalBudget": 1000000 },
      "schedule": { "startDate": "2026-11-28T00:00:00Z", "endDate": "2026-12-31T23:59:59Z" }
    }
    
  3. Activate when ready:
    PATCH /v1/partner/campaigns/:id   { "status": "ACTIVE" }
    
    Webhook: campaign.activated.
  4. Submit actions normally. The active campaign is matched automatically by actionType. (Or pass campaignId explicitly.)
  5. Monitor:
    • GET /v1/partner/campaigns/:id/analytics → stakeholder + tier breakdown, daily trends, budget %.
    • Subscribe to campaign.budget_reached and campaign.completed.
  6. Wind down: the campaign auto-completes at endDate, or you can PATCH … { status: "PAUSED" } early.

Scenario 5 — Top up your token pool when low

Goal: Catch low balances before they cause 402 errors in production.

Steps

  1. Subscribe to token_pool.low_balance on your webhook. Threshold is configurable on the pool.
  2. On webhook arrival, request more allocation:
    POST /v1/partner/token-pools/request
    {
      "amount": 500000,
      "reason": "Q4 forecast: 25% over allocation, current pool 8% remaining"
    }
    
  3. Wait for admin review. Webhook fires token_pool_request.approved (or …rejected).
  4. On approval, the pool's availableBalance increases. Subscribe to token_pool.refilled to confirm.

You can also enable auto-refill on the pool (admin-side) so this happens without manual intervention — autoRefill.enabled: true with a threshold, refill amount, and optional admin-approval gate.


Scenario 6 — Set up a webhook from scratch and verify it

# 1. Register
curl -X POST https://api.sirgiving.org/v1/partner/webhooks \
  -H "X-Partner-Key: $SK" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/sir",
    "eventTypes": ["action.completed", "action.failed"],
    "receiveAllEvents": false
  }'
# → save the returned `secret` securely

# 2. Send a test ping
curl -X POST https://api.sirgiving.org/v1/partner/webhooks/<id>/test \
  -H "X-Partner-Key: $SK" -H "X-Timestamp: $TS" -H "X-Signature: $SIG"

# 3. Inspect deliveries
curl "https://api.sirgiving.org/v1/partner/webhooks/<id>/deliveries?limit=20" \
  -H "X-Partner-Key: $SK" -H "X-Timestamp: $TS" -H "X-Signature: $SIG"

If a delivery shows status: FAILED, hit:

curl -X POST https://api.sirgiving.org/v1/partner/webhooks/<id>/deliveries/<deliveryId>/retry …

Scenario 7 — Health check before your daily batch

A 5-call preflight before submitting a 10k-action bulk run:

  1. GET /v1/partner/dashboard/integration-health — pipelines green?
  2. GET /v1/partner/token-pools/<id>/balance — enough headroom?
  3. GET /v1/partner/campaigns/active — right campaign live?
  4. GET /v1/partner/dashboard/webhooks/health — webhook success rate >99%?
  5. GET /v1/partner/dashboard/api-usage?days=1 — anywhere near rate limit?

Then submit in batches of 100 via POST /v1/partner/actions/bulk.


Common mistakes

  • Same idempotencyKey across two different actions. The second call will return the first action's response. Make keys unique per logical operation, e.g. order_98765 (not donation_2024).
  • Reusing publishable keys for write endpoints. They reject with 403 and you'll see INSUFFICIENT_SCOPE-style errors. Use sk_ for writes.
  • Forgetting actions:reverse scope. Reversal endpoint will 403 even with a valid sk_ key if the scope isn't on the key. Get the key re-issued with the scope.
  • Not handling 409 on retries. Idempotent POSTs can return your original result on retry — that's a feature, not a failure.
  • Ignoring X-RateLimit-Remaining. If you batch in tight loops, slow down preemptively rather than hitting 429s.
  • Webhook handler does work synchronously. Return 200 fast and offload to a queue. Slow handlers cause retries and can get your webhook auto-disabled.