Errors
When an API call fails, the status code tells you what kind of problem it is, and most errors also carry a JSON body with a readable message. This page lists the errors of every public endpoint, in the order the checks run.
These are errors of the API call itself. What happens when your target URL fails later, when the trigger fires, is described in How triggers fire.
Error format
Most errors come with Content-Type: application/json and a body like this:
{"_tag": "BadRequest","message": "Invalid URL: example.com"}
| Status code | _tag | Meaning |
|---|---|---|
| 400Bad Request | BadRequest | A header, field or value in the request is invalid. |
| 401Unauthorized | Unauthorized | The API key doesn't exist, or no credentials were sent. |
| 402Payment Required | QuotaExceeded | Your monthly quota is used up. Only /schedule returns it. |
| 404Not Found | NotFound | The trigger or tag policy doesn't exist. |
| 410Gone | Gone | The trigger exists but can no longer be edited or cancelled. |
_tagalways matches the status code, so you can branch on either.messageis meant for people and often contains the value that was rejected, such as the URL or the tag. Log it rather than parse it.- Only the first failed check is reported. After you fix it, the same request can fail on a later check.
Errors without a JSON body
Some errors are returned before the endpoint's own checks run, so they don't have the JSON body:
| Status code | Body | When |
|---|---|---|
| 400Bad Request | Empty | The request doesn't have the required shape: a required header is missing on /schedule or /cancel, or the body sent to /declare, /bulk or /tag-policies isn't valid JSON or doesn't match the expected fields and types. |
| 404Not Found | Empty | The path doesn't exist, or the endpoint doesn't accept the method, for example POST /cancel or GET /declare. |
| 415 | Plain text: Unsupported content-type: <type> | /declare, /bulk or /tag-policies received a Content-Type other than application/json. |
| 431 | Empty | The request headers are too large. See Size limits. |
Good to know
- Parse the body as JSON only when the response's
Content-Typeisapplication/json. The empty responses above have noContent-Typeat all. - The Full spec shows a JSON body for every
400. The empty-body400s above don't have one. - Requests over the size limits, or ones that aren't valid HTTP, may be rejected before they reach the API, with a response in a different format.
- A
HEADrequest to/schedulenever gets a response body, so you only see the status code, for errors too.
Handling errors
- Don't resend a request that got a
4xxwithout changing it first. A402lasts until your quota resets or your limit changes. - If you retry a
/cancelafter a timeout, a404or410can mean the first attempt already worked. See Cancelling again.
/schedule
Applies to creating triggers and to editing them. The checks run in the order of this table, and the first one that fails is returned:
| Status code | Message | When |
|---|---|---|
| 400Bad Request | (empty body) | ttr-api-keyheader or ttr-urlheader is missing. A key sent only as Authorization: Bearer counts as missing. |
| 400Bad Request | Failed to read request body | The request body couldn't be read completely. |
| 400Bad Request | Invalid URL: <ttr-url> | ttr-urlheader is empty or isn't an absolute URL. See Target URL. |
| 400Bad Request | Maximum 10 tags allowed | ttr-tagsheader lists more than 10 tags. |
| 400Bad Request | Tag too long (max 50 chars): <tag> | A tag is longer than 50 characters. |
| 400Bad Request | Invalid tag (only a-z 0-9 _ - allowed): <tag> | A tag contains another character, such as an uppercase letter or a space. See Tags. |
| 401Unauthorized | Invalid api key | ttr-api-keyheader is empty or doesn't match any key. |
| 400Bad Request | Invalid date: <date> | ttr-scheduled-atheader is empty, or its part before the first | is neither now nor a date. See Date formats. |
| 400Bad Request | Unknown pipe operation: <operation> | An operation after a | isn't a lowercase add or subtract with an amount. See Operations. |
| 400Bad Request | Invalid duration: <amount> | An amount isn't a whole number followed by s, m, h, d or w. |
| 400Bad Request | Invalid cron expression: <details> | The cron(...) expression or its time zone is invalid. For an unknown time zone, <details> may not mention the zone. See Cron syntax. |
| 402Payment Required | Monthly quota of <limit> triggers exceeded | Your project has used its monthly quota. See Quota. |
| 404Not Found | Trigger not found | You sent ttr-trigger-idheader with a date or now, and no one-shot trigger has this ID. This includes the ID of a recurring trigger. |
| 404Not Found | Generator not found | You sent ttr-trigger-idheader with cron(...), and no recurring trigger has this ID. This includes the ID of a one-shot trigger or of a single instance. |
| 410Gone | Job is no longer in registered state | You sent ttr-trigger-idheader with a date or now, and the one-shot trigger isn't registered anymore: it's skipped, queued, running, retrying, completed or cancelled. |
| 410Gone | Generator is no longer active | You sent ttr-trigger-idheader with cron(...), and the recurring trigger was cancelled. |
What the order means:
- An invalid URL or tag is reported even when the API key is wrong (
400). An invalidttr-scheduled-atheader with a wrong key gets401. - Tags are checked one at a time in the order you list them, length first, then characters. So
Bad,<a 51-character tag>getsInvalid tagforBad. - Calls rejected with
400,401or402don't use quota. A call that passes the quota check uses one unit, even if it then fails with404or410. 404and410only come from edits byttr-trigger-idheader. With a date ornow, if you also sendttr-custom-keyheader and it belongs to aregistered,skippedorretryingone-shot trigger, that trigger is edited and the ID isn't checked. Withcron(...), the ID is always checked. See Sending both identifiers.
/cancel
Only DELETE is accepted. A successful cancel returns 204 with an empty body. The checks run in the order of this table:
| Status code | Message | When |
|---|---|---|
| 400Bad Request | (empty body) | ttr-api-keyheader is missing. A key sent only as Authorization: Bearer counts as missing. |
| 400Bad Request | Must provide either ttr-trigger-id or ttr-custom-key header | Neither identifier was sent, or the one you sent is empty. |
| 400Bad Request | Provide either ttr-trigger-id or ttr-custom-key, not both | Both identifiers were sent. |
| 401Unauthorized | Invalid api key | ttr-api-keyheader is empty or doesn't match any key. |
| 410Gone | Generator already cancelled | By ID only: the recurring trigger with this ID is already cancelled. |
| 404Not Found | Job not found | Nothing in your project matches. By ID: no one-shot or recurring trigger has this ID. By custom key: no active recurring trigger and no uncancelled one-shot trigger has this key. The message is the same for recurring triggers. |
| 410Gone | Job is no longer cancellable (status=<status>) | The one-shot trigger is queued, running or completed, or, by ID, already cancelled. |
Good to know
- The identifiers are checked before the API key, so a request without an identifier gets
400even with a wrong key. /canceldoesn't use quota and never returns402.
/declare
The checks run in the order of this table:
| Status code | Message | When |
|---|---|---|
| 415 | Unsupported content-type: <type> (plain text) | The Content-Type isn't application/json. curl's -d sends application/x-www-form-urlencoded unless you set it. A request without a Content-Type is read as JSON. |
| 400Bad Request | (empty body) | The body isn't valid JSON or doesn't match the format: it's empty or not an object, tag, items or an item's url is missing, or a field has the wrong type, such as "runMissed": "true", or is null (only an item's body may be null). |
| 401Unauthorized | Invalid api key | The key in ttr-api-keyheader or Authorization: Bearer doesn't exist. |
| 401Unauthorized | Unauthorized | No API key, an empty one, or an Authorization header that isn't Bearer, and no dashboard session. |
| 400Bad Request | Duplicate customKey in items: <key> | Two items have the same customKey. |
| 400Bad Request | Item <key>: duplicate tag "<tag>" in tags | An item lists the same tag twice. |
| 400Bad Request | Item <key>: <error> | An item's scheduledAt can't be read. <error> is one of the ttr-scheduled-atheader messages of /schedule, for example Item (no customKey): Invalid date: tomorrow. |
Good to know
- The
Content-Typeand the body format are checked before your API key, so a malformed request gets415or400even without a valid key. - Items are checked in request order, before anything is written, and only the first error is returned.
<key>is(no customKey)for items without a key. /declarenever returns402,404or410. See Declare a set of triggers for what happens to the items.
/bulk
/bulk returns the same errors as /declare, with these differences:
| Status code | Message | When |
|---|---|---|
| 400Bad Request | (empty body) | Instead of a missing tag or items: an upsert has no url, a cancels entry isn't a string, {"id": ...}, {"customKey": ...} or {"tag": ...}, or upserts or cancels isn't a list. Both lists are optional, so {} is valid. |
| 400Bad Request | Duplicate customKey in upserts: <key> | Two upserts have the same customKey. |
| 400Bad Request | Upsert <key>: duplicate tag "<tag>" in tags | An upsert lists the same tag twice. |
| 400Bad Request | Upsert <key>: <error> | An upsert's scheduledAt can't be read, for example Upsert daily-report: Invalid cron expression: .... |
Cancel targets that match nothing, or only triggers that can't be cancelled anymore, are skipped without an error, so /bulk never returns 404 or 410. It never returns 402 either. See Bulk apply.
/tag-policies
The checks run in the order of this table:
| Status code | Message | When |
|---|---|---|
| 415 | Unsupported content-type: <type> (plain text) | POST and PUT: the Content-Type isn't application/json. |
| 400Bad Request | (empty body) | POST and PUT: the body isn't valid JSON or doesn't match the format: tag is missing on POST, a field has the wrong type (such as "maxConcurrent": "2"), a retryStrategy or retryOn value is unknown, or a throughput window is malformed (such as 1w). |
| 401Unauthorized | Invalid api key | The key in ttr-api-keyheader or Authorization: Bearer doesn't exist. |
| 401Unauthorized | Unauthorized | No API key, an empty one, or an Authorization header that isn't Bearer, and no dashboard session. |
| 404Not Found | Tag policy not found | PUT or DELETE: no policy in your project has this id. |
| 400Bad Request | See below | POST and PUT: a value is out of range. |
The 400 validation messages, in the order they are checked:
Invalid tag (only a-z 0-9 _ - allowed, max 50 chars)At least one of throughput, concurrency, timeout, or retry strategy must be specified(POSTonly)requestTimeoutSeconds must be between 1 and 3600maxConcurrent must be ≥ 1At most 8 throughput windows per policyThroughput rate for '<window>' must be a finite number ≥ 0Effective window for '<window>' at rate <rate> exceeds the 30-day maximum, followed by(rates below 1 stretch the window by 1/rate)when the rate is above 0 and below 1retryMaxAttempts must be ≥ 1 (or null for unlimited)retryInitialDelaySeconds is required (≥0) when retryStrategy is 'fixed' or 'exponential'retryMaxDelaySeconds must be ≥ 0retryInitialDelaySeconds must be ≤ 86400 (1 day)retryMaxDelaySeconds must be ≤ 86400 (1 day)
On PUT, the retry messages are checked against the policy as it would be after the update, so changing retryStrategy alone can fail if the stored policy has no initial delay. See Validation messages for the rules behind each message.