Errors

The pd4castr API uses conventional HTTP status codes to signal the outcome of a request. 2xx responses mean success, 4xx responses mean the request was rejected (most often because of a missing or expired token, an unknown identifier, or a malformed body), and 5xx responses mean the service couldn’t complete the request.

Every failed request returns the same JSON shape, carrying a machine-readable code that you branch on. Write one error handler around code and it works across the whole API.

Response shape

A failed request returns a JSON body with a code, a message, and, for some codes, a data object holding structured detail:

{
  "code": "NOT_FOUND",
  "message": "Model not found",
  "statusCode": 404,
  "error": "Not Found"
}
FieldTypeDescription
codestringThe stable, machine-readable identifier for the failure. Branch on this. See Error codes.
messagestringA human-readable description of what went wrong. The wording can change over time, so don’t match on it.
dataobjectOptional. Structured detail for the codes that carry it. Its shape depends on code, so branch before reading it.
statusCodenumberDeprecated. The HTTP status repeated in the body. See Deprecated fields.
errorstringDeprecated. The HTTP status reason phrase. See Deprecated fields.

message is always a single string. Validation failures used to send an array of strings here, one per rejected constraint. They now send one summary sentence, with the per-field breakdown under data.issues.

Error codes

Each code maps to exactly one HTTP status, so the status tells you the broad class of failure and the code tells you which specific failure it was. Most codes are generic and derived from the status; a few are domain codes that let you tell apart failures that share a status.

CodeStatusMeaning
BAD_REQUEST400The request was rejected for a reason with no more specific code. Read message.
VALIDATION_FAILED400A body field, query parameter, or path parameter failed validation. See Validation failures.
UNAUTHORIZED401The Authorization header is missing, or the bearer token is invalid or expired. Request a new token and retry once.
FORBIDDEN403Your organisation is authenticated but not permitted to perform this request.
ORGANISATION_DISABLED403Your organisation has been disabled. message carries the reason. Every authenticated request fails until it’s re-enabled.
NOT_FOUND404The model, model group, or run id doesn’t exist, or your organisation doesn’t have access to it. Retrying the same id won’t help.
REQUEST_TIMEOUT408The request took too long to complete. Transient, so retry with back-off.
CONFLICT409The request conflicts with the current state of the resource, such as triggering a run that’s already in flight.
MODEL_RUN_OUTPUT_UNAVAILABLE409The run’s output couldn’t be rebuilt after repeated attempts. Terminal: polling won’t change it.
RATE_LIMITED429You’ve exceeded your request allowance. Wait for the number of seconds in the Retry-After response header, then retry.
INTERNAL_ERROR500The service failed to handle the request. Retry with back-off; if it persists, contact support.
BAD_GATEWAY502An upstream dependency, usually the authorization server, was unreachable. Transient, so retry with back-off.

New codes are added as the API grows. Treat a code you don’t recognize as the generic failure for its HTTP status, and don’t reject the response for carrying an unknown one.

Validation failures

A VALIDATION_FAILED response describes every rejected input at once. message summarizes them in one sentence, and data.issues lists one entry per field, so you can map the failure back to the input that caused it:

{
  "code": "VALIDATION_FAILED",
  "message": "Request validation failed: limit must not be greater than 100",
  "data": {
    "issues": [
      {
        "field": "limit",
        "messages": ["limit must not be greater than 100"]
      }
    ]
  },
  "statusCode": 400,
  "error": "Bad Request"
}

Each issue has a field and a messages array. field uses dotted paths for nested body objects, such as window.from. A single field can fail more than one constraint, which is why messages is an array.

Token endpoint errors

The token endpoint, POST /v1/auth/token, is the one exception to the shape above. Because it implements the OAuth 2.0 client credentials grant, it returns OAuth error bodies instead:

{
  "error": "invalid_client",
  "error_description": "Client credentials were rejected."
}
errorStatusMeaning
invalid_request400A required field is missing or malformed. Fix the request; don’t retry as-is.
unsupported_grant_type400grant_type is missing or set to anything other than client_credentials.
invalid_client401Your client_id or client_secret is wrong or has been revoked. Stop and re-check your credentials.
temporarily_unavailable502The authorization server was unreachable, so no token could be issued. Transient, so retry with back-off.

Handling errors

Branch on code, and fall back to the HTTP status for anything you don’t recognize. A pragmatic handler covers five cases:

  • UNAUTHORIZED: request a fresh token from /v1/auth/token, then retry the original request once. If the retry fails the same way, your credentials are being rejected; treat it as invalid_client and stop.
  • VALIDATION_FAILED and BAD_REQUEST: fix the request and don’t retry. data.issues tells you which inputs were rejected.
  • NOT_FOUND: surface the failure to your caller rather than retrying with the same id.
  • RATE_LIMITED: wait for the Retry-After header’s value, then retry. Lower your request rate if you hit it repeatedly.
  • MODEL_RUN_OUTPUT_UNAVAILABLE: stop polling. The output can’t be rebuilt, so retrying returns the same response.

For everything else, including network timeouts, dropped connections, and unexpected 5xx responses, retry with exponential back-off starting at one second and cap the total attempts.

Deprecated fields

Error responses also carry statusCode and error, which is what the API sent before code existed. They stay in place during the transition so existing integrations keep working, and they’ll be removed once consumers have moved across.

  • statusCode repeats the HTTP status from the response’s status line.
  • error is the HTTP status reason phrase, such as Not Found. It’s derived from the status, so it can’t distinguish two failures that share one. code can, which is what replaces it.

If your integration switches on error today, move it to code. The mapping is one-way: every error value corresponds to one or more codes, so Not Found becomes NOT_FOUND, and Forbidden becomes either FORBIDDEN or ORGANISATION_DISABLED depending on why the request was rejected.

Next steps

  • Authentication is where you exchange your client credentials for a bearer token.
  • Quick Start is an end-to-end TypeScript example covering the full request lifecycle.