Errors.
Auth, Mail, Waitlist, Social, Trawl and the Platform API all answer a failed request with the same JSON body, so one error handler covers all of them.
The error body
{
"status_code": 404,
"statusCode": 404,
"message": "Webhook endpoint not found",
"code": "WEBHOOK_ENDPOINT_NOT_FOUND"
}| Field | Type | Meaning |
|---|---|---|
| status_code | number | The HTTP status of the response, repeated in the body. Always present. |
| message | string or string[] | A human-readable explanation. On a validation error, one message per failing field, each naming the field as you sent it. |
| code | string | A stable, machine-readable code. Always present. Branch on this, never on message. |
| details | object (optional) | Structured context for some errors, with snake_case keys: the unknown fields you sent, the limit you hit and when to retry. |
| statusCode | number (deprecated) | The same value as status_code. Kept so existing clients keep working. Read status_code in new code. |
Branch on code
Every error carries a code. An error specific to an operation has its own code, for example USERNAME_TAKEN, WEBHOOK_ENDPOINT_LIMIT or SERVICE_NOT_ENABLED; each API reference lists the codes an operation can return. Any other error gets the generic code for its status:
| Status | Generic code |
|---|---|
| 400 | BAD_REQUEST, or VALIDATION_ERROR when the request body or query failed validation |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 409 | CONFLICT |
| 413 | PAYLOAD_TOO_LARGE |
| 422 | UNPROCESSABLE_ENTITY |
| 429 | RATE_LIMITED |
| 500 | INTERNAL_SERVER_ERROR |
| 503 | SERVICE_UNAVAILABLE |
Messages are written for people and can change. Codes do not.
Validation errors
A request that fails validation gets a 400 with code: "VALIDATION_ERROR" and one message per problem. Each message names the field the way you sent it, in snake_case. A field the operation does not accept is rejected rather than ignored.
{
"status_code": 400,
"statusCode": 400,
"message": ["domain_id must be a UUID"],
"code": "VALIDATION_ERROR"
}statusCode is deprecated
Every other key in every ProductCraft API is snake_case. The error body used to be the exception: it carried the status as statusCode. It now carries status_code, and keeps statusCode with the same value so that nothing reading it breaks.
While the alias is sent, every error response also carries these headers (RFC 9745):
Deprecation: true
Link: <https://productcraft.co/docs/api/errors>; rel="deprecation"There is no removal date. If statusCode is ever removed, it will be announced in the changelog well ahead of time. Read status_code (or the HTTP status itself) in new code. The official SDKs already read the HTTP status, so they need no change.