Skip to content

Error handling

What the spec tells us (and what it doesn't)

Success responses are 201 for creates, 204 for transitions/deletes, 200 for reads. Error responses are JSON with a message (usually a code too); the shapes below are from stage. Log the message — it names the offending field.

Error bodies are JSON with a message, and usually a code:

{ "code": 404, "message": "Project (999999999) not found" }
{ "message": "Invalid API token provided" }

The message is genuinely useful — for a bad enum it even lists the allowed values:

{ "code": 400, "message": "Invalid JSON: [textType] Does not have a value in the enumeration [\"specialized-text\",\"product\",...]" }

How errors surface

Guzzle throws on 4xx and 5xx by default (http_errors is on), so a failed call is an exception, not a silent bad return:

use GuzzleHttp\Exception\ClientException;   // 4xx
use GuzzleHttp\Exception\ServerException;   // 5xx
use GuzzleHttp\Exception\RequestException;  // base (also network/timeout)

try {
    $item = $client->createItem(...);
} catch (ClientException $e) {
    $status = $e->getResponse()->getStatusCode();
    $body   = (string) $e->getResponse()->getBody();
    // log $body — it usually names the offending field. Don't retry a 4xx.
} catch (RequestException $e) {
    // network / timeout / 5xx — retryable with backoff
}

Reading the status code

Code Means Retry?
400 Bad payload — malformed JSON, missing required field, or a value outside an enum (bad textType, unmapped language). Message: Invalid JSON: [field] ... No — fix the request
401 Missing/invalid token, or a stage key against prod (or vice versa). Message: Invalid API token provided No — fix auth
403 Authenticated but not allowed to touch this resource No
404 No such project/item id. Message: Project (123) not found No (unless you raced a delete)
5xx Server-side / transient Yes — backoff and retry

Note: on stage, semantic rejections (bad enum value, unmapped language code) come back as 400, not 422 — the API folds validation into "Invalid JSON". So 400 covers both "your JSON is broken" and "your value isn't allowed"; read the message to tell them apart.

Catch the common ones before you send

Most failures are self-inflicted 4xx, and several are cheaper to catch client-side than to round-trip. The client already does this for:

  • Empty project nameInvalidArgumentException (the API requires a non-empty name anyway).
  • Unknown project typeInvalidArgumentException (only order/quote).
  • Unmapped language codeLanguageMap::toEurotext() throws before the request.
  • Invalid __meta valueInvalidArgumentException (flat key-value only).

textType is intentionally not validated client-side — see 06-text-types.md for why (it would drift). Validate it against getTextTypes() if you want to fail early.

Retries

The client doesn't retry for you — the right policy depends on your stack. Reads (GET /project, GET /item) are safe to retry freely. transitionProject and the info endpoints are safe too. Be careful re-running createProject/createItem blindly so you don't create duplicates — check what landed first or lean on cleanup (12-idempotency.md).

If you want automatic retries, Guzzle's RetryMiddleware slots into the handler stack you already pass into the constructor — add it there rather than wrapping every call.

Cleanup on partial failure

Delete works here because it runs immediately on failure, while the project is still a draft or freshly transitioned. Once the items are in-progress the project can no longer be deleted — so clean up at the point of failure, not later.

try {
    $project = $client->createProject('...', type: 'order');
    // add items, transition...
} catch (Throwable $e) {
    if (isset($project['id'])) {
        $client->deleteProject((int) $project['id']); // still a draft -> deletable
    }
    throw $e;
}

examples/submit_and_poll.php shows this inline.