One base URL, one auth header, JSON in and out. Everything you need is on this page.
https://sendreward.com/api/v1
Pass your API key as a bearer token. Keys are created in the dashboard under API keys and shown once.
Authorization: Bearer sr_live_a1b2c3…
Keys come in two modes. The mode is in the key itself, so there's no separate flag to set.
| Prefix | Behaviour |
|---|---|
| sr_live_ | Spends your prepaid balance and emails a real gift card. |
| sr_test_ | Spends sandbox balance ($1,000 per account, free). Nothing is emailed. Responses and statuses are identical to live. |
Every account starts with a test key. Build your whole integration against it; when you're ready, create a live key and swap the string. Test rewards appear in your dashboard under the Test filter.
curl https://sendreward.com/api/v1/rewards \ -H "Authorization: Bearer sr_test_…" \ -H "Idempotency-Key: signup-bonus-user-4412" \ -H "Content-Type: application/json" \ -d '{"recipient_email":"maria@example.com","recipient_name":"Maria","amount":25,"reference":"user-4412","message":"Thanks for completing our survey, Maria!"}'
Debits your balance and emails the recipient a reward they can redeem for their choice of brand. Returns the reward object with status sent.
| Field | Description |
|---|---|
| recipient_email required | Where the reward is sent. |
| amount required | USD, as a number. 25 or 25.00. Between $1.00 and $1,000.00. |
| recipient_name | Used in the email greeting. Recommended. |
| reference | Your own identifier (order ID, response ID). Searchable in the dashboard and filterable via the API. Up to 128 chars. |
| metadata | Any JSON object you want stored with the reward and echoed back. Not shown to the recipient. |
| message | A personal note shown in the email and on the redemption page. Up to 500 chars. Recommended — it's the difference between a gift and a transaction. |
| subject | Custom email subject line. Up to 150 chars. Defaults to the standard "You've received a reward" subject. |
| sender_name | Name shown as the email sender ("Acme Research via Tremendous"). Defaults to your company name. |
| campaign_id | Override the brand catalog and email template. Leave unset unless we've configured one for you. |
| Header | Description |
|---|---|
| Idempotency-Key | Strongly recommended. See Idempotency. |
201 Created{
"id": "rw_7f3a2c91d0e84b16",
"object": "reward",
"mode": "live",
"status": "sent",
"amount": "25.00",
"currency": "USD",
"recipient_email": "maria@example.com",
"recipient_name": "Maria",
"reference": "user-4412",
"metadata": null,
"error": null,
"created_at": "2026-09-17T14:02:11Z",
"updated_at": "2026-09-17T14:02:11Z"
}
402 insufficient_balance and nothing is sent. If the reward can't be delivered to our provider, you get 502 provider_error and your balance is not charged.Re-sends the reward email to the same recipient — for "it went to spam" or "I deleted it." No new charge. Only rewards with status sent or delivered can be resent, at most once every 10 minutes and 5 times in total. Returns the reward with resend_count incremented. If the address itself was wrong, cancel and send a new reward instead.
Cancels a reward the recipient has not yet redeemed. The reward link stops working and the full amount is returned to your balance immediately. Use this when an address was mistyped or a reward was sent by mistake — then send a new one. If the recipient has already redeemed, you get 409 not_cancelable and the status is updated to redeemed. Optional body: {"reason": "wrong address"}, stored for your records.
Returns the current state of a reward, including status changes as the recipient opens and redeems it.
| Query param | Description |
|---|---|
| limit | 1–100, default 50. |
| starting_after | A reward ID. Returns rewards created before it (cursor pagination, newest first). |
| status | Filter by status. |
| reference | Exact match on your reference. |
| recipient_email | Exact match. |
{ "object": "list", "data": [ … ], "has_more": true }
| Status | Meaning |
|---|---|
| sent | Accepted and handed to delivery. Your balance has been debited. |
| delivered | The email was delivered to the recipient's mailbox. |
| redeemed | The recipient chose a brand and claimed the card. |
| failed | Could not be delivered. Balance refunded automatically. error explains why. |
| canceled | Canceled before redemption — by you, the dashboard, or support. Amount refunded to your balance. |
{ "object": "balance", "mode": "live", "available": "1240.00", "currency": "USD" }
Returns live balance for live keys and sandbox balance for test keys.
Confirms your key works. Returns the key's mode and your company name.
Send an Idempotency-Key header with any value that's unique per reward — a survey response ID, an order number, a user ID plus event name. If the same key is sent again (a retry after a timeout, a duplicate webhook on your side, a double-click), you get the original reward back with a 200 and an Idempotent-Replayed: true header, and no second reward is sent.
Keys are scoped per API key mode and stored indefinitely. Up to 128 characters.
Errors are JSON with a stable code you can branch on and a message for humans.
{ "error": { "code": "insufficient_balance", "message": "Insufficient balance: available $12.00, required $25.00", "available": "12.00", "required": "25.00" } }
| HTTP | code | What to do |
|---|---|---|
| 401 | unauthorized | Missing, malformed, or revoked key. |
| 402 | insufficient_balance | Add funds in the dashboard, then retry with the same idempotency key. |
| 403 | account_suspended | Contact support. |
| 404 | not_found | No such reward or route. |
| 422 | invalid_request | A field is missing or out of range; the message says which. |
| 409 | not_resendable / not_cancelable | Reward isn't in a state that allows it (already redeemed, failed, or canceled). |
| 429 | resend_cooldown / resend_limit | Resent too recently, or the 5-resend cap was reached. |
| 429 | rate_limited | Back off and retry. See rate limits. |
| 502 | provider_error | Delivery provider rejected the reward. Not charged. Safe to retry. |
| 500 | internal_error | Our fault. Retry with the same idempotency key. |
120 requests per minute per API key. Every response includes X-RateLimit-Limit and X-RateLimit-Remaining. Need more? Email us — this is a soft limit.
Add an HTTPS endpoint under Notifications and we'll POST a JSON event whenever a reward changes state. Endpoints are per mode, so your staging server can receive test events only.
| Event | When |
|---|---|
| reward.sent | Immediately after a successful POST /v1/rewards. |
| reward.delivered | Email delivered to the mailbox. |
| reward.redeemed | Recipient claimed the card. |
| reward.failed | Delivery failed; balance refunded. |
| reward.canceled | Canceled before redemption; includes refunded amount. |
| reward.resent | The email was resent (by you, the dashboard, or support). |
| ping | Sent when you click "Send ping" in the dashboard. |
POST https://yourapp.com/webhooks/sendreward
Content-Type: application/json
X-SendReward-Signature: t=1758117731,v1=5f8c…
{
"id": "evt_3c1d…",
"event": "reward.redeemed",
"created_at": "2026-09-18T09:12:44+00:00",
"data": { …the reward object… }
}
Respond with any 2xx within 10 seconds. Anything else is retried with exponential backoff (2 min, 4 min, 8 min … up to 24 h) for 8 attempts. Handle events idempotently using id; the same event can arrive twice.
Each endpoint has a signing secret (shown once when you create it). Compute HMAC-SHA256(t + "." + raw_body, secret) and compare to v1 in constant time. Reject if t is older than 5 minutes.
$raw = file_get_contents('php://input');
$hdr = $_SERVER['HTTP_X_SENDREWARD_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $hdr), $p); // ['t' => …, 'v1' => …]
$ok = abs(time() - (int)$p['t']) < 300
&& hash_equals(hash_hmac('sha256', $p['t'] . '.' . $raw, $secret), $p['v1']);
if (!$ok) { http_response_code(400); exit; }
$event = json_decode($raw, true);
const crypto = require('crypto'); app.post('/webhooks/sendreward', express.raw({ type: '*/*' }), (req, res) => { const p = Object.fromEntries(req.get('X-SendReward-Signature').split(',').map(s => s.split('='))); const expected = crypto.createHmac('sha256', SECRET).update(p.t + '.' + req.body).digest('hex'); const ok = Math.abs(Date.now()/1000 - +p.t) < 300 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1)); if (!ok) return res.sendStatus(400); const event = JSON.parse(req.body); res.sendStatus(200); });
import hmac, hashlib, time p = dict(kv.split('=') for kv in request.headers['X-SendReward-Signature'].split(',')) expected = hmac.new(SECRET.encode(), f"{p['t']}.".encode() + request.get_data(), hashlib.sha256).hexdigest() if abs(time.time() - int(p['t'])) > 300 or not hmac.compare_digest(expected, p['v1']): abort(400) event = request.get_json()
Sending a reward from each language. No SDK required; it's one HTTP call.
$ch = curl_init('https://sendreward.com/api/v1/rewards');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $SENDREWARD_KEY,
'Idempotency-Key: survey-' . $responseId,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'recipient_email' => $email, 'recipient_name' => $name,
'amount' => 10, 'reference' => 'survey-' . $responseId,
]),
]);
$reward = json_decode(curl_exec($ch), true);
if (isset($reward['error'])) { // handle $reward['error']['code'] }
const res = await fetch('https://sendreward.com/api/v1/rewards', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.SENDREWARD_KEY}`, 'Idempotency-Key': `survey-${responseId}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ recipient_email: email, recipient_name: name, amount: 10, reference: `survey-${responseId}` }), }); const reward = await res.json(); if (reward.error) throw new Error(reward.error.code);
import requests, os r = requests.post('https://sendreward.com/api/v1/rewards', headers={'Authorization': f"Bearer {os.environ['SENDREWARD_KEY']}", 'Idempotency-Key': f'survey-{response_id}'}, json={'recipient_email': email, 'recipient_name': name, 'amount': 10, 'reference': f'survey-{response_id}'}) reward = r.json() if 'error' in reward: raise RuntimeError(reward['error']['code'])
curl https://sendreward.com/api/v1/rewards \ -H "Authorization: Bearer $SENDREWARD_KEY" \ -H "Idempotency-Key: survey-2291" \ -H "Content-Type: application/json" \ -d '{"recipient_email":"maria@example.com","recipient_name":"Maria","amount":10,"reference":"survey-2291"}'