Errors
Customer 360 uses conventional HTTP status codes. Successful responses wrap the payload in a { success, data } envelope. Business rejections use a structured error envelope with a stable machine code you can branch on. Auth failures and request-shape validation use FastAPI's detail shape.
The success envelope
Every successful call returns a JSON envelope:
successis alwaystrueon success.datais the payload (an object or an array), present on most responses.messageis an optional short human-readable note.
Success with data
{
"success": true,
"data": {
"accepted": 1,
"duplicates": 0,
"invalid": 0
}
}
The error envelope
A business rejection returns success: false with an error object carrying a stable machine code, a human message, and an optional detail. Branch on the code; it is the contract, while the message is human-facing.
Auth failures (401, 403, 429) use FastAPI's plain { "detail": "..." } shape instead, and the HTTP status carries the meaning. See Authentication & scopes.
Business error
{
"success": false,
"error": {
"code": "not_found",
"message": "No customer matches the supplied selector."
}
}
Business error with detail
{
"success": false,
"error": {
"code": "member_taken",
"message": "That memberId is already mapped to a different customer.",
"detail": {
"memberId": "SR-90210",
"ownerCustomerPublicId": "cus_zzz999"
}
}
}
Error codes
These are the stable machine codes and their HTTP status. A consumer should branch on the code, not the message text.
| Code | Status | Meaning |
|---|---|---|
not_found | 404 | the resource or selector matched nothing |
invalid_request | 422 | a malformed request |
invalid_selector | 422 | zero or more than one lookup selector supplied |
unknown_platform | 422 | the platform is not in the registry |
member_taken | 409 | (loyalty) the memberId is already mapped to a different customer |
Auth and rate-limit failures return { "detail": "..." } at 401, 403, and 429. Request-body validation failures return FastAPI's 422 with a detail array (below).
Per-event ingest results
Ingest is different: a bad envelope inside a batch is never an exception. POST /v1/events returns 200 when the request shape is valid, and reports each event's fate inline so one bad event never fails the batch. A result status is accepted, duplicate, or invalid; an invalid result carries an error string.
The invalid error strings come straight from the service. The common ones are an unknown type, a missing required payload field, a money field that does not parse or is negative, and an unknown or inactive platform.
Per-event results
{
"success": true,
"data": {
"results": [
{ "eventId": "ok-1", "status": "accepted" },
{ "eventId": "dup-2", "status": "duplicate" },
{ "eventId": "bad-3", "status": "invalid",
"error": "unknown event type 'foo'" },
{ "eventId": "bad-4", "status": "invalid",
"error": "missing required payload field 'currency'" }
],
"accepted": 1,
"duplicates": 1,
"invalid": 2
}
}
A duplicate result is a success, not an error: it means the (source, eventId) was already stored. That is exactly what makes retries and repair passes safe.
Validation errors
When the request body itself is malformed (a missing required field, a wrong type, an out-of-range query param), the service returns 422 with a structured detail array, FastAPI's standard validation shape. Each entry names the field location and why it failed, so you can map the error straight to the offending field.
Validation error (422)
{
"detail": [
{
"type": "missing",
"loc": ["body", "events", 0, "identity", "platform"],
"msg": "Field required",
"input": { "userId": "42" }
}
]
}