Core concepts
Errors
Perx separates transport failures from business outcomes. Knowing which is which is what stops your integration retrying things that will never succeed.
Error shape
{
"statusCode": 422,
"message": "Payload validation failed",
"code": "validation_failed",
"fields": [
{ "path": "transaction.items[0].unitPrice", "message": "must be a positive number with at most 2 decimal places" }
],
"requestId": "5f2c9a1e-0d47-4a2b-9c88-31ea7b40d6f1"
}Machine-readable codes
| code | What went wrong | Fix |
|---|---|---|
bad_signature | The HMAC did not match. | Almost always a re-serialised body. See Authentication. |
stale_timestamp | Timestamp outside the 300-second window. | Check NTP. On queued retries, re-sign with a fresh timestamp. |
unknown_connection | No active connection for that vendor + store. | The merchant has not connected, used a different store ID, or disconnected. |
store_mismatch | store.id differs from the X-Perx-Store header. | Usually a bug where one is hardcoded. |
validation_failed | The payload does not match the schema. | Read fields[] — it names each path. |
Perx validates strictly, so a typo like unit_price instead of unitPrice fails loudly during certification rather than silently zeroing a price in production. If you are getting 422 on a field you believe is correct, check the exact spelling and casing against the reference.
Business outcomes that are not errors
These all return 200. Treating any of them as a failure will cause pointless retries and false alerts.
| Outcome | Why it is a success |
|---|---|
Unknown customer (warning set) | The sale was recorded. The customer simply is not a Perx member yet — Perx invites them by SMS. |
status: "duplicate" | Idempotency worked. Your retry was absorbed and nothing was double-counted. |
status: "ignored" | An event type Perx does not act on. Accepted so you stop resending it. |
NOTHING_TO_REVERSE on a refund | The original sale never reached Perx or earned nothing. There is nothing to undo. |
Showing errors to cashiers
On the redemption endpoints, the message field on a 400 is written for the person at the till — “Minimum spend of MVR 300 not met”, not a stack trace. Display it verbatim. Replacing it with a generic “Something went wrong” is the difference between a cashier resolving the situation and a cashier calling support.
Getting help
Every response carries X-Request-ID, echoed as requestId in error bodies. Perx logs every request against it — quote it and support can trace the exact delivery in seconds. Without it, a report of “webhooks are failing” takes far longer to diagnose.