4. API Reference
API Reference
All paths below are relative to the base URL (https://api.sirgiving.org for production, https://devapi.sirgiving.org for sandbox).
Auth column legend:
- 🔐 HMAC — secret key + timestamp + signature
- 🔑 HMAC (sk) — same, but
pk_keys are explicitly rejected (@RequiresSecretKey) - 🌐 Widget — publishable key only (
pk_...), no signature - 🪪 JWT — partner-portal JWT (issued by
/auth/login)
Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
Partner Auth (portal)
These endpoints back the partner-user web portal, not server-to-server integration.
POST /v1/partner/auth/login
Email/password login for partner-portal users. Returns JWT pair + partner info + active publishable key.
GET /v1/partner/auth/me 🪪
Returns the authenticated partner user, their partner record, and active publishable key.
Partner Config 🌐
GET /v1/partner/config
Returns enabled features, rate limit, webhook URL, tip configuration. Used by widgets to bootstrap.
{
"id": "...",
"name": "Acme Inc",
"slug": "acme",
"enabledFeatures": ["DONATIONS", "ACTIONS"],
"rateLimit": 5000,
"isActive": true,
"webhookUrl": "https://acme.com/webhooks/sir",
"tipConfig": { "enabled": true, "defaultPercent": 10 }
}
Organizations 🌐
GET /v1/partner/organizations/:slug
Look up a nonprofit by slug (e.g. american-red-cross). Returns id, name, logo, EIN, status. Used by donation widgets.
Donations 🌐
POST /v1/partner/donations/create-link
Creates a payment link (Every.org or Stripe) attributed to your partner ID. Returns the URL plus a partnerDonationId to poll status with.
Request body:
{
"nonprofitSlug": "american-red-cross",
"amount": 1000, // cents
"currency": "USD",
"frequency": "once", // "once" | "monthly"
"provider": "every.org", // or "stripe"
"donorEmail": "donor@example.com",
"firstName": "Jane",
"lastName": "Doe",
"tipAmount": 100, // optional, Stripe path
"tipPercent": 10, // optional
"referralCode": "REFER123", // optional
"organizationId": "..." // optional, resolves from slug if omitted
}
Response:
{
"donationLink": "https://every.org/donate/american-red-cross?...",
"partnerDonationId": "550e8400-e29b-41d4-a716-446655440000",
"estimatedSirReward": 70
}
GET /v1/partner/donations/status/:partnerDonationId
Poll-able status: PENDING | PROCESSING | COMPLETED | FAILED plus tokensDistributed.
Actions 🔐 / 🔑
The core write API. An action is a unit of value (a donation made, hours volunteered, an advocacy email sent) that distributes SIR tokens to one or more stakeholders per the configured campaign.
POST /v1/partner/actions/submit 🔑
Submit a single action. Idempotent on idempotencyKey. Returns synchronously after token distribution.
Body fields:
| Field | Required | Notes |
|---|---|---|
idempotencyKey |
yes | ≤255 chars, unique per partner |
actionType |
yes | E.g. DONATION, VOLUNTEER, ADVOCACY |
amount |
yes | Numeric value (dollars/hours/points). >= 0 |
currency |
no | Default USD |
stakeholders[] |
yes | See below |
campaignId |
no | Defaults to partner's default campaign |
tokenPoolId |
no | Defaults to partner's default pool |
autoCreateUsers |
no | Default true. Auto-creates partner users by userEmail |
metadata |
no | Free-form JSON |
Stakeholder shape:
{
"stakeholderTypeCode": "DONOR",
"partnerUserId": "user_123", // your external ID
"userEmail": "user@example.com", // for auto-create
"userFirstName": "Jane",
"userLastName": "Doe",
"organizationId": "12-3456789", // EIN, for ORG stakeholders
"amount": 100, // override; usually derived from campaign split
"index": 0, // for multiple of same type
"metadata": {}
}
Status codes:
200— completed (response body hasactionId,status,tokensDistributed,transactionIds[])400— validation error402— insufficient pool balance409— duplicate idempotency key (returns the original cached result with200-equivalent body)429— rate limited
GET /v1/partner/actions/:id 🔐
Action details by ID. 404 if not yours.
POST /v1/partner/actions/:actionId/reverse 🔑
Trigger full or partial reversal. Requires actions:reverse scope on the API key.
{
"reversalPercentage": 100, // 0–100; 100 = full
"reason": "Refunded by customer service",
"refundIdempotencyKey": "rev_donation_2024_..."
}
Response includes tokensClawedBack, debtsCreated (when stakeholder balance is insufficient), and a per-stakeholder breakdown.
GET /v1/partner/actions 🔐
List with page, limit (max 100), status, actionType, fromDate, toDate.
POST /v1/partner/actions/bulk 🔑
Submit up to 100 actions for sequential synchronous processing. Response:
{
"successful": 8,
"failed": 2,
"results": [
{ "idempotencyKey": "k1", "status": "COMPLETED", "actionId": "..." },
{ "idempotencyKey": "k2", "status": "FAILED", "error": "..." }
]
}
GET /v1/partner/actions/bulk/:jobId 🔐
Deprecated. Returns 410 Gone. Bulk is synchronous now — read the POST response.
Users 🔐
Users are keyed by your externalUserId. We mirror them with a SIR account/wallet.
GET /v1/partner/users 🔐
Paginate (page, limit, search).
POST /v1/partner/users 🔐
Create. Idempotent on externalUserId — returns existing user if already created.
{
"externalUserId": "user_123",
"email": "user@example.com",
"firstName": "Jane",
"lastName": "Doe",
"metadata": {}
}
Response includes partnerUserId, accountId, walletCreated, upgradeEligible.
GET /v1/partner/users/:externalId 🔐
PATCH /v1/partner/users/:externalId 🔐 — update email/name/metadata
GET /v1/partner/users/:externalId/balance 🔐
Returns totalBalance, availableBalance, restrictedBalance, and balanceByRestriction[] (with availableAt for vested chunks).
GET /v1/partner/users/:externalId/transactions 🔐
The user's action history scoped to your partner. Supports page, limit, status, actionType, fromDate, toDate.
Token Pools 🔐
GET /v1/partner/token-pools 🔐
Lists allocated pools. Filter by status.
GET /v1/partner/token-pools/:id/balance 🔐
Pool balance breakdown: allocated, available, reserved, distributed, percent available.
POST /v1/partner/token-pools/request 🔐
Submit a request for additional allocation (admin-reviewed).
{ "amount": 100000, "reason": "Q3 campaign launch" }
GET /v1/partner/token-pools/requests 🔐
List your past allocation requests.
Campaigns 🔐
GET /v1/partner/campaigns 🔐 — list with status filter
GET /v1/partner/campaigns/active 🔐 — currently-running campaign for widget banners
GET /v1/partner/campaigns/:id 🔐
GET /v1/partner/campaigns/:id/analytics 🔐 — full analytics (stakeholder + tier breakdown, daily trends)
POST /v1/partner/campaigns 🔐 — create from template
{
"tokenPoolId": "...",
"name": "Holiday 2X",
"description": "Double rewards for Q4",
"templateId": "REWARD_MULTIPLIER",
"actionTypes": ["DONATION"],
"distribution": {
"baseMultiplier": 2.0,
"stakeholderSplits": [
{ "stakeholderTypeId": "DONOR", "percentage": 70, "isRequired": true, "splitMode": "EQUAL" },
{ "stakeholderTypeId": "ORGANIZATION", "percentage": 30, "isRequired": true, "splitMode": "EQUAL" }
],
"tiers": [
{ "minAmount": 0, "maxAmount": 50, "multiplier": 1.0 },
{ "minAmount": 50, "maxAmount": null, "multiplier": 2.0 }
]
},
"limits": { "maxDistributionPerAction": 1000, "dailyDistributionLimit": 50000, "totalBudget": 1000000 },
"schedule": { "startDate": "2024-11-01T00:00:00Z", "endDate": "2024-12-31T23:59:59Z" }
}
PATCH /v1/partner/campaigns/:id 🔐
Only DRAFT and ACTIVE are mutable. Completed campaigns return 400 CAMPAIGN_NOT_ACTIVE.
Transactions 🔐
A transaction is the double-entry ledger record produced by an action's token distribution.
GET /v1/partner/transactions 🔐
Filter by status, actionId, tokenPoolId, fromDate, toDate.
GET /v1/partner/transactions/:id 🔐
Returns full ledger detail: per-stakeholder entries[] with debit/credit/restrictionType, totalDebits, totalCredits, isBalanced, failureReason.
Webhooks 🔐 / 🔑
See the Webhooks page for delivery format and verification.
POST /v1/partner/webhooks 🔑 — register
{
"url": "https://your-app.example.com/webhooks/sir",
"description": "prod webhook",
"eventTypes": ["action.completed", "action.failed", "transaction.completed"],
"receiveAllEvents": false,
"headers": { "X-My-Tenant": "prod" }
}
Response includes the signing secret — store it; you'll need it to verify deliveries.
GET /v1/partner/webhooks 🔐 — list
DELETE /v1/partner/webhooks/:id 🔑 — unregister
GET /v1/partner/webhooks/:id/deliveries 🔐
Filter by status (PENDING/DELIVERED/FAILED/RETRYING), eventType, paginate with limit/offset.
POST /v1/partner/webhooks/:id/deliveries/:deliveryId/retry 🔐
Manually re-queue a failed delivery for immediate redispatch.
POST /v1/partner/webhooks/:id/test 🔐
Sends a partner.status_changed ping with { type: 'ping', message, webhookId, timestamp } body.
Dashboard 🔐
Read-only analytics for partner-side dashboards.
| Endpoint | What it returns |
|---|---|
GET /v1/partner/dashboard/summary |
Pool utilization, activity counts, user totals |
GET /v1/partner/dashboard/trends?days=30 |
Daily series: actions, tokens, unique users, success rate (max 365d) |
GET /v1/partner/dashboard/top-earners?limit=10 |
Top users by tokens earned (max 100) |
GET /v1/partner/dashboard/webhooks/health |
Last-24h webhook success rate, avg response time, recent failures |
GET /v1/partner/dashboard/api-usage?days=7 |
Daily request counts, rate-limited counts, error counts, top endpoints (max 90d) |
GET /v1/partner/dashboard/recent-activity |
Last 10 actions, newest first |
GET /v1/partner/dashboard/integration-health |
Webhook + token pool + action pipeline status |
GET /v1/partner/dashboard/campaigns/:id/analytics |
Campaign performance breakdown |
Error envelope
All errors return:
{
"error": "ERROR_CODE",
"message": "Human readable message",
"requestId": "req_abc123",
"retryAfter": 60 // only on 429
}
Common codes: INVALID_API_KEY, INVALID_SIGNATURE, TIMESTAMP_EXPIRED, PARTNER_NOT_ACTIVE, PARTNER_SUSPENDED, RATE_LIMIT_EXCEEDED, INVALID_REQUEST, INSUFFICIENT_SCOPE, USER_NOT_FOUND, ACTION_NOT_FOUND, CAMPAIGN_NOT_FOUND, POOL_NOT_FOUND, TRANSACTION_NOT_FOUND, WEBHOOK_NOT_FOUND, CAMPAIGN_NOT_ACTIVE, ENDPOINT_GONE, INTERNAL_ERROR.