Skip to content
Sursa Developers

Errors

Sursa uses HTTP status codes to say whether a request succeeded, and a stable error code to say why it did not. Every error response has the same shape:

{
  "detail": "the cursor is malformed or was issued for different filters",
  "error": {
    "code": "api.cursor_invalid",
    "message": "the cursor is malformed or was issued for different filters",
    "details": {"parameter": "cursor"},
    "request_id": "01JB7Q2M4X9V3K8T5N6P0R1S2A",
    "doc_url": "https://developers.sursa.com/errors/api.cursor_invalid"
  }
}
FieldMeaning
error.codeStable code in the form <area>.<reason>. Branch your code on it.
error.messageA readable English explanation. It can be reworded at any time; do not parse it.
error.detailsStructured facts about the error, such as the parameter, a limit, or when to retry. Its fields depend on the code.
error.request_idThe ID of this request, the same as the X-Request-Id response header.
error.doc_urlThe page about this code on this portal.
detailThe earlier form of the error, kept for existing integrations. New code should read error.

Every code has its own page under Error codes, with its HTTP status, the fields of details, and the endpoints that return it. The page of each endpoint in the API reference lists the codes that endpoint can return.

HTTP statuses

StatusMeaning
200, 201, 202, 204The request succeeded. 202 means it was accepted and is still being processed.
400The request is malformed: a bad parameter, cursor, metadata or Idempotency-Key. Fix the request.
401No valid key. See Authentication.
402A limit of the organization's plan is reached, such as the number of cards or storage.
403The key is valid but not allowed to do this: a role, a scope or a plan feature is missing.
404The object does not exist, or the key is not allowed to see it.
409The request conflicts with the current state: a duplicate external_id, an open sync, a request with the same Idempotency-Key still running.
410The object has expired, such as an upload after 24 hours, or the route has been retired.
413The body or the file is too large.
415The file type is not supported, or its content does not match its name.
422The request is well formed but invalid: a missing field or a value out of range.
429Too many requests or a quota is used up. See Rate limits and quotas.
500, 502, 503, 504A problem on the Sursa side. Retry later.

Validation errors

For 422 with the code validation.failed, detail lists every field that failed, each with its location:

{
  "detail": [
    {"loc": ["body", "title"], "msg": "String should have at least 1 character", "type": "string_too_short"}
  ],
  "error": {"code": "validation.failed", "message": "request validation failed", "request_id": "01JB7…"}
}

Which errors to retry

ErrorRetry?
Network error or timeout, no responseYes, with the same Idempotency-Key.
429Yes, after the number of seconds in Retry-After.
500, 502, 503, 504Yes, with exponential backoff: 1 s, 2 s, 4 s… up to a minute, with random jitter.
409 api.idempotency_in_progressYes, after Retry-After: the first request with this key is still running.
Any other 4xxNo. The same request will fail again; fix it first.

Retry POST and PATCH requests only with an Idempotency-Key, so a retry after a lost response cannot create a second object. GET, PUT and DELETE are safe to retry as they are.

// Retries a request on network errors, 429 and 5xx. The same Idempotency-Key is sent on every attempt.
async function withRetries(send, attempts = 5) {
  for (let attempt = 1; ; attempt++) {
    let response;
    try {
      response = await send();
    } catch (error) {
      if (attempt >= attempts) throw error;
    }
    if (response && response.status !== 429 && response.status < 500) return response;
    if (attempt >= attempts) return response;
    const retryAfter = Number(response?.headers.get("Retry-After"));
    const backoff = Math.min(60, 2 ** (attempt - 1)) * (0.8 + Math.random() * 0.4);
    await new Promise((resolve) => setTimeout(resolve, 1000 * (retryAfter || backoff)));
  }
}

const key = crypto.randomUUID();
const response = await withRetries(() =>
  fetch("https://api.sursa.com/v1/cards/text", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SURSA_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": key,
    },
    body: JSON.stringify({ title: "Refund policy", body_markdown: "Refunds take 14 days." }),
  }),
);

Reporting a problem

Every response, successful or not, has an X-Request-Id header, and every error has the same value in error.request_id. Log it with your own record of the request. When you contact support about a request, send its request ID: it leads straight to the record of that request.

Handling codes you do not know

New error codes can appear in any version of the API. Treat an unknown code by its HTTP status: an unknown 4xx as a request that will not succeed as it is, an unknown 5xx as a temporary problem.