SponsorPaydocs
POS integration guide

Connect your POS in an afternoon.

Scan a barcode, apply your own promotion, report the sale. Three calls and you're integrated. Your POS keeps control of item eligibility.

Start with the flowGet API keys

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.

type: coupon
It's a discount

You get a percent. Apply the POS promotion you configured earlier, and it reduces the sale.

totalPercent → posDiscountId
type: giftCard
It's a payment

You get an amount. Apply it as tender against the balance due.

amount → tender
Start here

The whole integration is 3 steps

Everything below is detail. If you read one thing, read this.

1GET
Scan the barcode

Look up the code. Read-only, nothing is charged, safe to call repeatedly.

/api/store/barcode/:code
2POS
Apply your promotion

Coupons: apply the posDiscountId promotion. Gift cards: apply the balance as tender.

use posDiscountId
3POST
Report the sale

After the sale finalizes, send the totals. This commits and credits your store wallet.

/api/store/coupon/redeem

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.

Production
https://api.sponsorpay.org
Demosafe for testing
https://api.demo.sponsorpay.org

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
Getting a 401?

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.

SUCCESS
{ "ok": true, ...data }
FAILURE
{ "ok": false, "error": "message" }
All three redeem and void endpoints return HTTP 200 even on business failures

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

Real HTTP status codes are used only for
StatusMeaning
401Missing or invalid API key → { "ok": false, "error": "Unauthorized" }.
400Malformed request. A validation failure returns error as a { fieldErrors, formErrors } object; a non-integer void id returns it as the string "Valid redemption id is required."

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.

GET/api/store/barcode/:code

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.

Optional query param
ParamDescription
phone optionalIf sent and the coupon requires a phone, the scan validates it early and returns active: false with reason COUPON_PHONE_MISMATCH when it doesn't match.
Response — active coupon
{
  "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
  }
}
FieldDescription
posDiscountIdID of the matching promotion in your POS. Auto-apply this. null if not configured.
totalPercentTotal discount % to apply.
sponsorPercent / storePercentHow the % splits. Reporting only — apply totalPercent.
requirePhoneIf true, prompt the customer for their phone before redeeming.
phoneLast3Last 3 digits of the registered phone. Show it so the customer can confirm. null if none on file.
maxAmountDiscountAppliesCap on how much of the sale the discount can apply to, as a 2-decimal string. Limit your totalAmountDiscountApplies to this. null = no cap.
maxDiscountThisSaleCap on the total discount dollars for this sale, as a 2-decimal string. null = no cap.
couponId / discountId / sponsorIdInternal IDs, for your reference and support tickets.
Response — active gift card
{
  "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).

Response — not found
{
  "ok": true,
  "found": false,
  "active": false,
  "type": null,
  "reason": "NOT_FOUND"
}
Response — coupon not redeemable
{
  "ok": true,
  "found": true,
  "type": "coupon",
  "active": false,
  "reason": "COUPON_NOT_ACTIVE"
}
Coupon reason codes when active: false
ReasonMeaning
COUPON_NOT_ACTIVEAlready used or voided
COUPON_REDEMPTION_LIMIT_REACHEDUsed the max number of times
COUPON_AMOUNT_LIMIT_REACHEDSponsor budget exhausted
COUPON_SALE_LIMIT_REACHEDCumulative sale amount limit reached
COUPON_DISCOUNT_LIMIT_REACHEDCumulative discount dollar limit reached
COUPON_STORE_LIMIT_REACHEDSingle-use per store; already used here
COUPON_PHONE_MISMATCHA phone query param was sent and doesn't match
DISCOUNT_NOT_ACTIVECampaign not active
DISCOUNT_EXPIREDCampaign expired
DISCOUNT_OPTION_INACTIVEStore's discount option is off or not approved
DISCOUNT_OPTION_EXPIREDStore's discount option expired
SPONSOR_NOT_ACTIVESponsor account inactive
STORE_INACTIVEYour store account inactive
POST/api/store/coupon/redeem

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

Request body
{
  "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"
}
Success response
{
  "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 */ }
}
Save the redemption ID

Store redemption.id in your POS transaction record. You need it to void.

Request fields
FieldTypeNotes
code requiredstringThe scanned coupon barcode.
saleId requiredstringYour POS's unique sale ID. Prevents duplicate redemptions.
totalSaleAmount requirednumberFull sale total before discount.
totalItems requiredintegerNumber of items in the sale.
totalAmountDiscountApplies requirednumberThe portion of the sale your promotion applied the discount to. Do not exceed maxAmountDiscountApplies from the scan.
totalDiscount requirednumberThe actual discount dollars your POS applied. Server validates against its own calculation, otherwise you get TOTAL_DISCOUNT_MISMATCH.
roundedDiscount optionalbooleantrue if your POS rounded to a whole dollar. Widens the match tolerance. Default false.
phone optionalstring10 digits. Required when requirePhone: true. Must match the registered phone.
storeDiscountAdjustmentPercent optionalintegerZero or negative (<= 0). Reduces the store's % contribution, e.g. -5. Default 0.
storeDiscountAdjustmentReason optionalstringRequired when the adjustment is non-zero, must be omitted when it's 0. One of PAID_CC or OTHER.
registerId / cashierId optionalstringTerminal and cashier IDs, for reporting.
metadata1–3 optionalstringFree-form reference fields.

Unknown fields are rejected with a 400 validation error.

How totalDiscount is validated

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

Business failures — HTTP 200 with ok: false
ErrorMeaning
"Coupon not found."Barcode not valid at this store
"Coupon is not active."Already used or voided
"Store is not active." / "Sponsor is not active."Account inactive
"Discount is not active." / "Discount is expired."Campaign issue
"Discount option is not active." / "...expired." / "...not approved."Store option issue
"Phone is required." / "Phone does not match."Phone verification failed
"Duplicate sale."saleId already redeemed at this store
"Coupon redemption limit reached."Too many uses
"Coupon amount limit reached."Sponsor budget gone
"Coupon total sale limit reached." / "...total discount limit reached."Cumulative caps hit
"Coupon has already been used at this store."Single-use per store
"Discount redemption limit reached." / "Discount amount limit reached."Campaign-level caps
"Total discount does not match."totalDiscount doesn't match server calculation
"Insufficient funds."Sponsor wallet empty
POST/api/store/redemption/discount/:id/void

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

Request
POST .../discount/1001/void

{ "reason": "Sale voided" }

// reason is optional, stored
// for the audit trail
Success response
{
  "ok": true,
  "redemption": {
    "id": 1001,
    "status": "VOIDED",
    "voidedAt": "2026-05-07T21:30:00Z",
    "voidReason": "Sale voided"
  }
}
Errors — business failures are HTTP 200, check ok
HTTPErrorMeaning
400"Valid redemption id is required."id is not a positive integer — the only real 400
200"Discount redemption not found."ID doesn't exist or belongs to another store
200"Only committed redemptions can be voided."Already voided or failed
200"Redemption can only be voided within 24 hours."Void window closed
200"Redemption can no longer be voided."Wallet credit already settled
POST/api/store/giftCard/redeem

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

Request body
{
  "code": "GC-123",
  "amount": 20.00,
  "saleId": "TXN-48292",
  "phone": "2125551111",
  "registerId": "REG-1",
  "cashierId": "CASHIER-7"
}
Success response
{
  "ok": true,
  "redemption": {
    "id": 901,
    "status": "COMMITTED",
    "amount": "20",
    "saleId": "TXN-48292",
    "createdAt": "2026-05-07T20:02:04Z"
  },
  "totals": { "amount": "20" }
}
Request fields
FieldTypeNotes
code requiredstringGift card code.
amount requirednumberAmount to redeem. Minimum 0.01, must not exceed the remaining balance from the scan.
saleId requiredstringUnique sale ID for duplicate prevention.
phone optionalstring10 digits. Required when requirePhone: true.
registerId / cashierId optionalstringFor reporting.
Business failures — HTTP 200 with ok: false
ErrorMeaning
"Gift card not found."Code not valid at this store
"Gift card is not active."Already fully redeemed or voided
"Gift card is expired."Past expiry date
"Gift card amount limit reached."Requested amount exceeds remaining balance
"Store is not active." / "Sponsor is not active."Account inactive
"Phone is required." / "Phone does not match."Phone verification failed
"Duplicate sale."saleId already used
"Insufficient funds."Sponsor wallet empty

Retry & duplicate safety

If a redeem request times out with no response, retry with the exact same saleId.

Original succeeded

Retry returns "Duplicate sale." — it worked the first time. Treat as success.

Original truly failed

The retry processes normally, as a new request.

Never retry an ok: false business error

Those results are deterministic. Retrying won't change the outcome.

Stuck on something?

For API keys, POS setup, or a question about any endpoint, we'll help you get running.

Contact SponsorPay