Quickstart — your first points in 15 minutes
By the end of this page you will have awarded points on a real sandbox card and seen them land. Everything here is copy-paste runnable.
What you need
Three things, all from us:
| Your test key | Looks like cardy_pk_test_…. It only ever reaches the sandbox — there is no way for a test key to touch live customer balances. |
| A test card | Serial 947706-732-680, on a real sandbox customer. Your test card prints it as a QR your scanner can read. |
| A test customer | The phone number that card belongs to: 96599901001. Same person, second way in — a customer who left their card at home still has to be servable. |
The card is the one to start with. Your till identifies people by scanning, and the serial is what a scanner reads — so the scan path is the one worth getting right first. The phone is the fallback a cashier types.
The sandbox lives at:
https://sandbox.getcardy.net/api/v1
Every request needs your key in the Authorization header. Set it once:
export CARDY_KEY="cardy_pk_test_replace_me"
export CARDY_URL="https://sandbox.getcardy.net/api/v1"
1. Scan the card
This is what your till does the moment a customer presents their card. Send
whatever the scanner read, as loyalty_id:
curl -s -X POST "$CARDY_URL/customers/identify" -H "Authorization: Bearer $CARDY_KEY" -H "Content-Type: application/json" -d '{"loyalty_id":"947706-732-680"}'
There is nothing to parse and no prefix to strip: the serial on the card is the
value we resolve. It is also the card_id you will see in the response — one
string, whether it reached you from a scanner or from an earlier identify.
If the customer left the card at home, the same call takes their phone instead, and answers with the same customer:
curl -s -X POST "$CARDY_URL/customers/identify" -H "Authorization: Bearer $CARDY_KEY" -H "Content-Type: application/json" -d '{"phone":"96599901001"}'
Send exactly one identifier. Two would be ambiguous — if they named different people, either answer could award points to the wrong one — so we refuse rather than choose.
You get the customer and every card they hold:
{
"customer_id": "01a03fca-ffa8-70b4-b850-428496841903",
"cards": [
{
"card_id": "947706-732-680",
"template_name": "Demo Restaurant (points per KWD)",
"balances": { "points": 99, "stamps": null, "visits": null },
"allowed_operations": ["earn_amount", "redeem_points", "redeem_reward"]
}
]
}
allowed_operations is the field to build your buttons from. It lists what
this card can actually do. If redeem_points is not in the list, do not show a
redeem button — the card cannot do it, and the call would be refused.
Balances are null for dimensions a card does not have. A stamp card has no
points; that is different from having zero points.
2. Award points for a sale
curl -s -X POST "$CARDY_URL/transactions/earn" -H "Authorization: Bearer $CARDY_KEY" -H "Content-Type: application/json" -d '{"identifier":{"loyalty_id":"947706-732-680"},"card_id":"947706-732-680","external_ref":"receipt-1001","amount_minor":12500,"currency":"KWD"}'
{ "transaction_id": "cmtc1kl6w0000qs01bt4prksp", "status": "pending", "duplicate": false }
Three fields deserve attention.
The scan appears twice, and it is not a typo. identifier says who this
is; card_id says which card the points go on. They carry the same string
because you scanned one card belonging to one person — but they answer different
questions, and a customer holding four cards makes that obvious. Leave card_id
out and we choose from everything they hold, which is right for a phone lookup
and wrong for a scan: the cashier is holding the card the customer chose.
external_ref is your receipt number. Send your own — whatever identifies
this sale in your system. It is how you look the transaction up later, how you
refund it, and how we stop a retry from paying twice.
amount_minor is an integer. KWD has three decimals, so 12.500 KWD is
12500. Never send a decimal; never assume two places.
status: "pending" means we have accepted the sale and are applying it. The
customer's balance updates within a second or two. Your till does not need to
wait — the sale is already recorded.
3. Prove it landed
Look the sale up by the receipt number you sent:
curl -s "$CARDY_URL/transactions?external_ref=receipt-1001" -H "Authorization: Bearer $CARDY_KEY"
{
"data": [
{
"transaction_id": "cmtc1kl6w0000qs01bt4prksp",
"intent": "earn",
"status": "success",
"external_ref": "receipt-1001",
"card_id": "144389-894-749",
"amount_minor": 12500,
"currency": "KWD",
"points": null
}
]
}
status has moved from pending to success — the sale has been applied.
For an end-of-day or accounting pull, ask for a window instead of a receipt.
from is inclusive and to exclusive, so one call per day tiles cleanly, and
channel tells you which rows your system sent and which your staff made in
the Cardy dashboard:
curl -s "$CARDY_URL/transactions?from=2026-10-20T00:00:00%2B03:00&to=2026-10-21T00:00:00%2B03:00&limit=100" -H "Authorization: Bearer $CARDY_KEY"
Follow next_cursor until it comes back null.
card_id is the card you scanned, because you named it. Had you left it out, we
would have picked from the customer's cards ourselves — and where more than one
could genuinely take the sale, answered VALIDATION_ERROR with
multiple_eligible_cards rather than guessing which balance to move.
points is null while amount_minor is set, because we record what you sent
us. The card's own balance moved in whatever unit that card keeps — scan it
again and you will see it.
4. Now retry it
Send the exact same earn request from step 2 a second time.
{ "transaction_id": "cmtc1kl6w0000qs01bt4prksp", "status": "success", "duplicate": true }
Same transaction, duplicate: true, and the customer was not paid twice.
This is the most important thing to understand about this API. Your till will
lose connectivity mid-sale, and it will not know whether we got the request.
The answer is always the same: send it again with the same external_ref.
It cannot double-award. You never need to reconcile, ask us, or write logic to
guess what happened.
The one way to break this is to generate a new external_ref on retry. Don't.
The receipt number is the same receipt number.
Where to go next
- Error handling — every code, what the cashier should see, and what your code should do.
- Certification — what we check before you go live, and how to run those checks yourself.
- Postman collection — every endpoint, ready to import.