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
actionIdagainst your order so you can later reverse it on a refund. - Listen for
action.completedif you want server-side confirmation rather than trusting the response.
Failure modes
402 INSUFFICIENT_POOL— your token pool is empty. Resolve byPOST /v1/partner/token-pools/requestfor more allocation.409 IDEMPOTENT— sameidempotencyKeywas already processed; you get the original response back. Safe to retry.429— rate-limited. HonorRetry-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
partnerDonationIdagainst the user session to show a "Thanks for donating!" page on return. - Subscribe to
action.completedandaction.failedserver-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:reversein 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
- Pick a token pool to fund the campaign:
GET /v1/partner/token-pools→ identify the pool with enough headroom. - 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" } } - Activate when ready:
Webhook:PATCH /v1/partner/campaigns/:id { "status": "ACTIVE" }campaign.activated. - Submit actions normally. The active campaign is matched automatically by
actionType. (Or passcampaignIdexplicitly.) - Monitor:
GET /v1/partner/campaigns/:id/analytics→ stakeholder + tier breakdown, daily trends, budget %.- Subscribe to
campaign.budget_reachedandcampaign.completed.
- Wind down: the campaign auto-completes at
endDate, or you canPATCH … { status: "PAUSED" }early.
Scenario 5 — Top up your token pool when low
Goal: Catch low balances before they cause 402 errors in production.
Steps
- Subscribe to
token_pool.low_balanceon your webhook. Threshold is configurable on the pool. - On webhook arrival, request more allocation:
POST /v1/partner/token-pools/request { "amount": 500000, "reason": "Q4 forecast: 25% over allocation, current pool 8% remaining" } - Wait for admin review. Webhook fires
token_pool_request.approved(or…rejected). - On approval, the pool's
availableBalanceincreases. Subscribe totoken_pool.refilledto 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:
GET /v1/partner/dashboard/integration-health— pipelines green?GET /v1/partner/token-pools/<id>/balance— enough headroom?GET /v1/partner/campaigns/active— right campaign live?GET /v1/partner/dashboard/webhooks/health— webhook success rate >99%?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
idempotencyKeyacross two different actions. The second call will return the first action's response. Make keys unique per logical operation, e.g.order_98765(notdonation_2024). - Reusing publishable keys for write endpoints. They reject with 403 and you'll see
INSUFFICIENT_SCOPE-style errors. Usesk_for writes. - Forgetting
actions:reversescope. Reversal endpoint will 403 even with a validsk_key if the scope isn't on the key. Get the key re-issued with the scope. - Not handling
409on 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.