Errors
One error envelope, a fixed set of codes, and a request id on every response.
Every error from the typeship API uses the same envelope:
{
"errors": [
{ "code": "invalid_request", "message": "name is required." }
],
"request_id": "req_k3j2h8f0a1b2c3d4"
}errors is always an array, even for a single error. request_id also arrives on every response as the x-request-id header. Quote it when reporting a problem.
Codes
| Code | Status | Retry | When |
|---|---|---|---|
invalid_request | 400 | No | Malformed JSON, a failed field validation, a bad cursor, or an operation that does not apply (regenerating a repository-sourced project on demand). The message names the field. |
unauthorized | 401 | No | Missing, malformed, or revoked API key. Fix the credential first. |
plan_limit_reached | 402 | No | The account has used its included hosted generations. The response names where to lift it. |
not_found | 404 | No | The resource does not exist or belongs to another account. The two are indistinguishable on purpose. |
payload_too_large | 413 | No | A spec over 10MB, inline or fetched. |
spec_error | 422 | No | The document cannot be used as a spec at all. The message is the generator's, quoted verbatim. Warnings never cause this. See Errors and warnings. |
fetch_error | 400 or 502 | 502 only | The spec URL was invalid (400) or could not be fetched (502): unreachable, timed out after 15 seconds, or answered with an error status. |
rate_limited | 429 | After Retry-After | Too many requests. A Retry-After header says how long to wait. See Rate limits. |
internal_error | 500 | Once, with backoff | The generator hit an unexpected condition. Not a problem with your spec. |
Agent guidance
Read errors[0].code, never the message, and act on the table above. Three cases deserve care:
plan_limit_reachedand anonymous caps are not failures to retry. An anonymousPOST /v1/generatesucceeds with alimitsobject (max_operations,omitted_operations,reason,signup_url,upgrade_url); a 402 carries the same idea for hosted generation. Tell the user what was left out and where the cap lifts; do not loop on the same call.rate_limitedsays how long. Wait theRetry-Afterseconds once. Anonymous calls also carryX-RateLimit-LimitandX-RateLimit-Remainingon every response, so pace before the wall, not after.- Large results are paged and indexed. A generation over the size cap returns
files_omitted: truewith afiles_index; fetch the files you need withGET /v1/generations/{id}/file?path=...instead of asking for the whole generation again. Lists returnhas_moreandnext_cursor.
The typeship CLI turns all of this into its own envelope with stable codes and next_steps (below); the typeship MCP server returns the same bodies as tool errors.
In the typeship SDK
Nothing throws. Every call returns a discriminated result, and the error side is a typed union of the documented errors for that operation. The envelope is the typed body:
const result = await client.projects.create({ name: "" });
if (!result.ok) {
result.error.status; // 400
result.error.body.errors[0]; // { code: "invalid_request", message: "name can't be empty." }
result.response?.requestId; // "req_..."
}PaymentRequiredError, NotFoundError, UnprocessableEntityError, and the rest are exported by name. unwrap(result) throws them instead. See Results and errors for the pattern, which is the same one every SDK typeship generates uses.
In the typeship CLI
Errors print one JSON envelope on stderr, {status, issues: [{code, message}], docs_url, next_steps, detail}, and exit 1; the API's envelope above rides along as detail.body, and the typeship CLI's issues[].code is derived from the status (NO_AUTH, PLAN_LIMIT, RATE_LIMITED, SPEC_INVALID, and so on). Usage mistakes exit 2 before any request is made. See Output and exit codes.