Cardy docs

Errors

Every error looks the same:

{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Not enough points on this card.",
    "retryable": false,
    "request_id": "req_34e6710244c0411790e74fc0f31548bf"
  }
}

Branch on code, never on message. Codes are stable — we will never rename one or change what it means. Messages are for humans and may be reworded or translated.

Log request_id. It is the one thing that lets us find your exact request if you need to ask us about it.

retryable is a promise, not a hint

When retryable is true, sending the identical request again — same external_ref — cannot double-apply. Retry with backoff and stop worrying.

When it is false, retrying will fail the same way. Something has to change first: a different card, a smaller amount, a corrected field.

The codes

Code Retry? What happened What the cashier should see What your code should do
VALIDATION_ERROR
400
no The request was malformed. details names the fields. "Something went wrong — try again." Fix the request. This is a bug on your side; log details loudly.
UNAUTHENTICATED
401
no The key is missing, wrong, revoked or expired. We do not say which. "Terminal not set up." Stop. Check the key. Do not retry in a loop.
TENANT_SUSPENDED
403
no The store's account is suspended, usually billing. "Loyalty is paused — contact the manager." Stop sending. The sale still completes; loyalty simply does not apply.
CUSTOMER_NOT_FOUND
404
no No customer matches that phone, email or card. "Not a member yet — sign them up?" Offer enrollment. Never treat this as an outage.
COUPON_ALREADY_REDEEMED
409
no That coupon has been used. "This coupon has already been used." Do not retry.
ALREADY_ENROLLED
409
no That phone or email is already a member. The existing customer is in the body. "Already a member — here's their card." Switch to their card and carry on. Do not ask the cashier to search again.
RESERVATION_EXPIRED
410
no The held balance timed out before you committed it. "Basket expired — scan again." Re-reserve, then commit.
CARD_NOT_ELIGIBLE
422
no This card cannot do this operation. "Not available on this card." Build buttons from allowed_operations and you will not hit this.
INSUFFICIENT_BALANCE
422
no Not enough points, stamps or credit. "Not enough points." Show the balance from identify.
OTP_INVALID
422
no The code was wrong, expired, or too many tries. details[0] says which. "That code is not valid." Read details[0]: otp_incorrect, otp_expired, otp_attempts_exhausted, enrollment_not_found.
TEMPLATE_MECHANICS_UNKNOWN
422
no The card is configured in a way we cannot route. "Not available on this card." Nothing you can fix — tell us. This is our configuration problem.
RATE_LIMITED
429
yes Too many requests. Nothing. Back off for Retry-After seconds, then retry.
INTERNAL
500
yes Our bug. "Something went wrong — try again." Retry with backoff. Same external_ref.
UPSTREAM_TIMEOUT
504
yes Our loyalty provider was slow. "Taking longer than usual." Retry with the same external_ref. It cannot double-apply.

DUPLICATE_TRANSACTION is not an error

You will see "duplicate": true at HTTP 200 with the original result. That is a success: we recognised the receipt number and returned what we did the first time. Treat it exactly like a first-time success.

The two mistakes worth naming

Generating a new external_ref when you retry. This is the only way to double-award a customer, and no error code will tell you it happened — both requests succeed. The receipt number identifies the sale, not the attempt.

Showing a button allowed_operations did not list. Every operation in that list will work on that card. Anything not in it will be refused. If you build your buttons from the list you will never see CARD_NOT_ELIGIBLE.

Cardy Loyalty API Questions your integration cannot answer belong in an email, not a retry loop.