SendReward

API reference

One base URL, one auth header, JSON in and out. Everything you need is on this page.

https://sendreward.com/api/v1

Authentication

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.

PrefixBehaviour
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.

Sandbox and test keys

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.

Quickstart

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!"}'

Send a reward

POST/v1/rewards

Debits your balance and emails the recipient a reward they can redeem for their choice of brand. Returns the reward object with status sent.

Request body

FieldDescription
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_nameUsed in the email greeting. Recommended.
referenceYour own identifier (order ID, response ID). Searchable in the dashboard and filterable via the API. Up to 128 chars.
metadataAny JSON object you want stored with the reward and echoed back. Not shown to the recipient.
messageA 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.
subjectCustom email subject line. Up to 150 chars. Defaults to the standard "You've received a reward" subject.
sender_nameName shown as the email sender ("Acme Research via Tremendous"). Defaults to your company name.
campaign_idOverride the brand catalog and email template. Leave unset unless we've configured one for you.

Headers

HeaderDescription
Idempotency-KeyStrongly recommended. See Idempotency.

Response 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"
}
If your balance can't cover the amount you get 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.

Resend a reward

POST/v1/rewards/{id}/resend

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.

Cancel a reward

POST/v1/rewards/{id}/cancel

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.

Retrieve a reward

GET/v1/rewards/{id}

Returns the current state of a reward, including status changes as the recipient opens and redeems it.

List rewards

GET/v1/rewards
Query paramDescription
limit1–100, default 50.
starting_afterA reward ID. Returns rewards created before it (cursor pagination, newest first).
statusFilter by status.
referenceExact match on your reference.
recipient_emailExact match.
{ "object": "list", "data": [ … ], "has_more": true }

Statuses

StatusMeaning
sentAccepted and handed to delivery. Your balance has been debited.
deliveredThe email was delivered to the recipient's mailbox.
redeemedThe recipient chose a brand and claimed the card.
failedCould not be delivered. Balance refunded automatically. error explains why.
canceledCanceled before redemption — by you, the dashboard, or support. Amount refunded to your balance.

Balance

GET/v1/balance
{ "object": "balance", "mode": "live", "available": "1240.00", "currency": "USD" }

Returns live balance for live keys and sandbox balance for test keys.

Ping

GET/v1/ping

Confirms your key works. Returns the key's mode and your company name.

Idempotency

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

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" } }
HTTPcodeWhat to do
401unauthorizedMissing, malformed, or revoked key.
402insufficient_balanceAdd funds in the dashboard, then retry with the same idempotency key.
403account_suspendedContact support.
404not_foundNo such reward or route.
422invalid_requestA field is missing or out of range; the message says which.
409not_resendable / not_cancelableReward isn't in a state that allows it (already redeemed, failed, or canceled).
429resend_cooldown / resend_limitResent too recently, or the 5-resend cap was reached.
429rate_limitedBack off and retry. See rate limits.
502provider_errorDelivery provider rejected the reward. Not charged. Safe to retry.
500internal_errorOur fault. Retry with the same idempotency key.

Rate limits

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.

Webhooks

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.

EventWhen
reward.sentImmediately after a successful POST /v1/rewards.
reward.deliveredEmail delivered to the mailbox.
reward.redeemedRecipient claimed the card.
reward.failedDelivery failed; balance refunded.
reward.canceledCanceled before redemption; includes refunded amount.
reward.resentThe email was resent (by you, the dashboard, or support).
pingSent 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.

Verifying signatures

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);

Code examples

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'] }