Quick start
Grab a demo key from the store portal and scan a code. If you get ok: true, your auth works.
# 1. scan a code (read-only, nothing is charged) curl https://api.demo.sponsorpay.org/api/store/barcode/ABC123 \ -H "x-api-key: YOUR_KEY"
# 2. after the sale finalizes, report it
curl -X POST https://api.demo.sponsorpay.org/api/store/coupon/redeem \
-H "x-api-key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "ABC123",
"saleId": "TXN-48291",
"totalSaleAmount": 100.00,
"totalItems": 3,
"totalAmountDiscountApplies": 100.00,
"totalDiscount": 30.00
}'Two codes, two behaviours
The scan tells you which one you have. Everything downstream follows from that.
You get a percent. Apply the POS promotion you configured earlier, and it reduces the sale.
You get an amount. Apply it as tender against the balance due.
The whole integration is 3 steps
Everything below is detail. If you read one thing, read this.
Look up the code. Read-only, nothing is charged, safe to call repeatedly.
Coupons: apply the posDiscountId promotion. Gift cards: apply the balance as tender.
After the sale finalizes, send the totals. This commits and credits your store wallet.
If the cashier voids the sale afterwards, call POST /api/store/redemption/discount/:id/voidto reverse everything. That refunds the sponsor's wallet and cancels your pending credit.
posDiscountId, the key to full automation
When a store sets up a Discount Option in the portal, they enter the ID of a promotion already configured in your POS (a Promotion in Clover, a Discount in Square, an Applied Discount in Toast). That promotion already has the item and category rules baked in.
The scan returns that posDiscountId. You auto-apply the promotion, your POS decides eligibility, and SponsorPay only ever sees the final dollar amounts you report.
You never send us line items. Eligibility is entirely your POS's job.
Environments
Two environments, same API. Build against demo, ship to production.
Create demo accounts on store.demo.sponsorpay.org / sponsor.demo.sponsorpay.org. Calls behave identically to production.
Authentication
One API key per store, sent as a header on every request. No login session needed. The key is tied to your store, so you never send a storeId. Generate keys in the store portal under API Keys; the raw key is shown only once on creation.
# every request x-api-key: YOUR_KEY
The key is missing, wrong, or deactivated. Body is { "ok": false, "error": "Unauthorized" }. Regenerating a key instantly invalidates the old one.
Response format
Every response has an ok field.
{ "ok": true, ...data }{ "ok": false, "error": "message" }/coupon/redeem, /giftCard/redeem and /redemption/discount/:id/void answer 200 with ok: false for things like "Coupon is not active.", "Insufficient funds." or "Only committed redemptions can be voided." Always check ok, never the status code, on these three.
All money amounts and percentages are strings in every POS response, including the barcode scan: amounts as 2-decimal strings ("30.00"), percentages as integer strings ("15", "-5"). Dates are UTC ISO strings.
Scan a barcode
Scan any coupon barcode or gift card code. Read-only, no charge, safe to call multiple times. Returns type of coupon or giftCard.
{
"ok": true,
"found": true,
"type": "coupon",
"active": true,
"data": {
"couponId": 88,
"discountId": 12,
"sponsorId": 3,
"code": "ABC123",
"requirePhone": false,
"phoneLast3": null,
"totalPercent": "30",
"sponsorPercent": "15",
"storePercent": "15",
"posDiscountId": "CLOVER-PROMO-47",
"maxAmountDiscountApplies": null,
"maxDiscountThisSale": null
}
}{
"ok": true,
"found": true,
"type": "giftCard",
"active": true,
"data": {
"giftCardId": 55,
"code": "GC-123",
"amount": "25.00",
"requirePhone": false,
"phoneLast3": null
}
}amount is the remaining balance as a 2-decimal string, and the maximum you can redeem. When a gift card is revoked or fully redeemed you get active: false, reason: "NOT_ACTIVE", and data.amount is the remaining balance ("0.00" when fully spent).
{
"ok": true,
"found": false,
"active": false,
"type": null,
"reason": "NOT_FOUND"
}{
"ok": true,
"found": true,
"type": "coupon",
"active": false,
"reason": "COUPON_NOT_ACTIVE"
}active: falseRedeem a coupon
Submit a completed coupon sale after it's finalized in your POS. This commits the redemption, charges the sponsor's wallet, and credits yours.
{
"code": "ABC123",
"saleId": "TXN-48291",
"totalSaleAmount": 100.00,
"totalItems": 3,
"totalAmountDiscountApplies": 100.00,
"totalDiscount": 30.00,
"roundedDiscount": false,
"phone": "2125551111",
"registerId": "REG-1",
"cashierId": "CASHIER-7"
}{
"ok": true,
"redemption": {
"id": 1001,
"status": "COMMITTED",
"saleId": "TXN-48291",
"discountAmount": "30.00",
"sponsorDiscountAmount": "15.00",
"storeDiscountAmount": "15.00",
"createdAt": "2026-05-07T20:02:04Z"
},
"totals": { /* same three amounts */ }
}Store redemption.id in your POS transaction record. You need it to void.
Unknown fields are rejected with a 400 validation error.
totalDiscount is validatedThe server validates your totalDiscount against its own calculation. If it doesn't match you get TOTAL_DISCOUNT_MISMATCH.
If your POS rounds the discount to whole dollars, send roundedDiscount: true — it widens the server's match tolerance. When you do hit a mismatch, the usual cause is that the discount was applied to a different subtotal than the one you reported in totalAmountDiscountApplies.
ok: falseVoid a redemption
Call when the cashier voids the sale. Pass the redemption.idfrom the redeem response. The sponsor's wallet is refunded and your pending credit is cancelled.
POST .../discount/1001/void
{ "reason": "Sale voided" }
// reason is optional, stored
// for the audit trail{
"ok": true,
"redemption": {
"id": 1001,
"status": "VOIDED",
"voidedAt": "2026-05-07T21:30:00Z",
"voidReason": "Sale voided"
}
}okRedeem a gift card
Gift cards carry a dollar balance instead of a percentage, applied as payment tender. Scan first to get the remaining balance, then call this after the sale is finalized.
{
"code": "GC-123",
"amount": 20.00,
"saleId": "TXN-48292",
"phone": "2125551111",
"registerId": "REG-1",
"cashierId": "CASHIER-7"
}{
"ok": true,
"redemption": {
"id": 901,
"status": "COMMITTED",
"amount": "20",
"saleId": "TXN-48292",
"createdAt": "2026-05-07T20:02:04Z"
},
"totals": { "amount": "20" }
}ok: falseRetry & duplicate safety
If a redeem request times out with no response, retry with the exact same saleId.
Retry returns "Duplicate sale." — it worked the first time. Treat as success.
The retry processes normally, as a new request.
ok: false business errorThose results are deterministic. Retrying won't change the outcome.