Skip to content
Docs
About By Dawn Contact supportOpen By Dawn
  1. Build reliably
  2. Responses and errors

Responses and errors

Every answer from the API, whether it worked or not, has the same shape. So your code needs just one check to know whether a call worked, and one field to know what went wrong. This page shows that shape, the errors you’ll meet most, and which ones are worth retrying.

We call the shape the envelope. When a call works, ok is true and what you asked for is in data:

{ "ok": true, "data": { "id": "ticket-…", "identifier": "SAW-42", "title": "Export the board as CSV" } }

When it fails, ok is false and error holds a slug: a short, fixed code such as ticket_not_found. Sometimes a message comes with it, with extra detail for your logs:

{ "ok": false, "error": "ticket_not_found" }

Write your error handling against the slug. Slugs are part of the API’s contract: they don’t change without notice, and any change is recorded in the API changelog. The message wording and the HTTP status text can change at any time, so don’t parse them.

Status error What happened What to do
401 invalid_api_key The key exchange was refused: the key is wrong, revoked or expired. Check the key, or create a new one.
401 unauthorized The access token is missing or has expired. Exchange the key for a new token, once.
403 forbidden The key isn’t allowed to do this, in this organization. Check the key’s scopes.
404 not_found, ticket_not_found, project_not_found There’s no such thing in this organization. Don’t retry.
422 validation_error, invalid_status, invalid_project, … Something in the request isn’t valid. message says which field. Fix the request. Don’t retry.
429 rate_limited Too many requests in the last minute. Wait Retry-After seconds, then retry. See rate limits.

The API reference lists the errors each operation can return, and the full set of slugs is in its ApiErrorSlug and AccountsApiErrorSlug schemas. If you meet a slug you don’t recognise, handle it the way you’d handle its HTTP status.

Retry only when waiting can change the answer:

  • 401 unauthorized: exchange the key for a fresh token once, and repeat the request. If that fails too, stop, because the key itself has been revoked or has expired.
  • 429 rate_limited: wait the Retry-After seconds, then send the same request again.

Everything else will fail the same way next time. Retrying a 404 or 422 just repeats the mistake, and retrying a 401 or 403 in a loop won’t fix a wrong key or a missing scope.

Like every recipe, this one checks each response and turns a failure into an error that carries the status, the slug and the message. Then it sets off three real failures on purpose and handles each one by its slug. It writes nothing: the one ticket it tries to create is refused.

Read down the steps. The code on the right follows along and highlights the lines each one explains.

  1. Every recipe starts this way, as the Quickstart explains.

    // 1. Exchange the API key for a short-lived access token. Every call after this goes to your
    // organization's URL with the token as a Bearer header.
    const exchange: Schemas['ApiKeyExchange'] = { apiKey };
    const { accessToken: token } = await call<Schemas['AccessTokenData']['data']>(`${ACCOUNTS_URL}/auth/token/api-key`, { method: 'POST', body: exchange });
    const api = `${WORKSPACE_URL}/orgs/${orgIdOf(token)}`;
  2. The recipe asks for a project that isn’t there. Depending on the operation, the slug is the specific project_not_found or the general not_found, so it handles both. A 404 won’t change if you ask again, so there’s nothing to retry.

    // 2. A resource that does not exist: 404.
    const missing = await failure(call(`${api}/projects/project-00000000-0000-0000-0000-000000000000`, { token }));
    switch (missing.error) {
    case 'project_not_found':
    case 'not_found':
    console.log(`${missing.status} ${missing.error}: no such project in this organization.`);
    break;
    default:
    throw missing;
    }
  3. Next it tries to create a ticket with an empty title. The slug, validation_error, is what the code checks. The message says which field is wrong, which is for the person reading your logs, so the recipe prints it rather than parsing it.

    // 3. An invalid request: 422. The message says what is wrong; it is for people, so log it, don't parse it.
    const [project] = await call<Schemas['Project'][]>(`${api}/projects`, { token });
    if (!project) throw new Error('This organization has no projects yet.');
    const invalid = await failure(call(`${api}/tickets`, { method: 'POST', body: { projectId: project.id, title: '' }, token }));
    if (invalid.error !== 'validation_error') throw invalid;
    console.log(invalid.message);
  4. Last, it tries to exchange a key that doesn’t exist. The accounts host refuses it with invalid_api_key, which is your cue to check the key or create a new one, not to try again.

    // 4. An API key that is wrong or revoked: the token exchange is refused.
    const wrongKey: Schemas['ApiKeyExchange'] = { apiKey: 'saw_live_not_a_real_key' };
    const refused = await failure(call(`${ACCOUNTS_URL}/auth/token/api-key`, { method: 'POST', body: wrongKey }));
    if (refused.error !== 'invalid_api_key') throw refused;
    console.log(`${refused.status} ${refused.error}: check the key, or create a new one in Settings → Security.`);

Run the TypeScript recipe from this page. See Try it in your browser.

Try it with your organization

How this works

Connect once and run this recipe live against the By Dawn API.

Your key stays in this tab and goes only to By Dawn. Use a read-only key in a test organization.

▶ Run errors.tsThe same code CI runs, in your browser
Connect your organization above to run it.