Errors

The error envelope, what each status means, and when to retry.

Every error has the same shape, and the HTTP status tells you whose move it is. 4xx means the request needs to change before it can succeed. 429 and 5xx mean try the same request again later.

JSON
{
  "error": {
    "type": "invalid_request_error",
    "code": "unknown_field",
    "message": "requirements.directors is not a field in the SG PRIVATE_LIMITED_COMPANY schema (version 2026-09-01).",
    "param": "requirements.directors",
    "requestId": "req_01J9ZKM2QF7C4D"
  }
}
  • type: the broad class of error, one per status. Listed below.
  • code: the specific reason, stable and safe to branch on. Each endpoint's reference page lists the codes it returns.
  • message: a sentence for developers. It may change wording at any time; do not parse it or show it to end users as-is.
  • param: the field or path the error is about, or null when it is not about one input.
  • requestId: quote it when you contact support. Log it with every failed request.

The types:

  • invalid_request_error: 400
  • authentication_error: 401
  • not_found_error: 404
  • conflict_error: 409
  • validation_error: 422
  • rate_limit_error: 429
  • api_error: 500 and other 5xx

400 Bad request

The request is malformed or contains something the endpoint does not accept. Fix the request; retrying it unchanged will fail again.

JSON
{
  "error": {
    "type": "invalid_request_error",
    "code": "unknown_field",
    "message": "requirements.directors is not a field in the SG PRIVATE_LIMITED_COMPANY schema (version 2026-09-01).",
    "param": "requirements.directors",
    "requestId": "req_01J9ZKM2QF7C4D"
  }
}
  • unknown_field: the body has a key the endpoint does not accept. This is how PATCH/incorporations/{incorporationId}/intake rejects any key under requirements that is not a field in the incorporation's schema, so a typo fails loudly instead of being dropped. The usual cause is sending data about people or companies through intake: that data belongs on persons and business entities.
  • missing_file: an upload to POST/files had no file part.

A file larger than 20 MB is refused with status 413 and code file_too_large.

401 Unauthorized

The API key is missing, malformed, revoked, or not a GetStarted secret key.

JSON
{
  "error": {
    "type": "authentication_error",
    "code": "invalid_api_key",
    "message": "The API key provided is not valid.",
    "param": null,
    "requestId": "req_01J9ZKM5RG8D5E"
  }
}

Check that the header is Authorization: Bearer gs_test_… or Authorization: Bearer gs_live_…, that the key has not been rotated out, and that your deployment loaded the right environment variable. Do not retry until the key is fixed. See Authentication.

404 Not found

The object does not exist in this environment.

JSON
{
  "error": {
    "type": "not_found_error",
    "code": "resource_not_found",
    "message": "No incorporation with ID inc_01J9ZK7C3VYB5N exists in this environment.",
    "param": "incorporationId",
    "requestId": "req_01J9ZKM7SH9E6F"
  }
}

409 Conflict

The request is valid but clashes with the current state of an object. Re-read the object, then decide.

JSON
{
  "error": {
    "type": "conflict_error",
    "code": "incorporation_locked",
    "message": "Incorporation inc_01J9ZK7C3VYB5N has been submitted. Only fields named in an open issue can be changed.",
    "param": "requirements.businessActivity",
    "requestId": "req_01J9ZKM9TJ1F7G"
  }
}
  • incorporation_locked: the incorporation has been submitted. Intake, participants and document slots are read-only from then on, except for whatever an open issue's path points at. Check GET/incorporations/{incorporationId}/issues to see what is editable.
  • incorporation_cancelled: you tried to submit an incorporation that was cancelled. Start a new one.
  • participant_exists: that person or entity is already a participant on this incorporation. Change its roles with PATCH/incorporations/{incorporationId}/participants/{participantId} instead of creating it again.
  • duplicate_external_reference: another customer in this environment already has that externalReference. Look it up with GET/customers filtered by externalReference; you probably created it already.
  • registry_field_locked: you tried to change a registry-issued field, such as the registrationNumber of an entity formed through GetStarted.

422 Unprocessable

The request is well formed, but a value is not acceptable or the object is not ready for the action.

JSON
{
  "error": {
    "type": "validation_error",
    "code": "validation_incomplete",
    "message": "The incorporation cannot be submitted until every requirement is met.",
    "param": null,
    "requestId": "req_01J9ZKMB2K2G8H",
    "missingRequirements": [
      {
        "path": "participants.par_01J9ZKE7LB2H6J.documents.PROOF_OF_ADDRESS",
        "message": "A PROOF_OF_ADDRESS document is required for each DIRECTOR."
      }
    ],
    "invalidRequirements": [
      {
        "path": "requirements.companyNameOptions",
        "code": "unique_items",
        "message": "Each company name option must be different."
      }
    ]
  }
}
  • validation_incomplete: returned by POST/incorporations/{incorporationId}/submit when the draft is not complete. The error carries the same missingRequirements and invalidRequirements lists as GET/incorporations/{incorporationId}/validation, so you can route the user to each problem without a second call. See Requirements for the path grammar.
  • invalid_value: a value fails type or format checks, such as a date that is not YYYY-MM-DD or a phone number that is not E.164. param names the field.
  • type_unavailable: the type is in the catalog but has available: false.
  • role_not_allowed: the role is not in the schema's participantRequirements, or the participant type is not allowed for it.
  • document_kind_not_required: none of the participant's roles need that documentKind.
  • unsupported_file_type: the upload is not a PDF, JPEG or PNG.
  • invalid_url: a webhook URL is not HTTPS or does not resolve.

429 and 5xx: retry

429 rate_limit_error means you are sending too many requests. 500 api_error and other 5xx responses mean something failed on our side. Both are safe to retry, and so are network timeouts.

  • Retry with exponential backoff and jitter: wait about 1s, 2s, 4s, 8s, with some randomness, and give up after a handful of attempts.
  • Send an Idempotency-Key on every POST and reuse it across retries. If the first attempt actually succeeded before the connection dropped, the retry returns that result instead of creating a duplicate. See Idempotency.
  • Do not retry 4xx responses other than 429. They will fail the same way.
async function withRetry(doRequest, { attempts = 5 } = {}) {
  for (let attempt = 0; ; attempt++) {
    let res;
    try {
      res = await doRequest();
    } catch (err) {
      if (attempt >= attempts - 1) throw err; // network error
    }
    if (res && res.status !== 429 && res.status < 500) return res;
    if (attempt >= attempts - 1) return res;
    const delayMs = Math.min(30_000, 1000 * 2 ** attempt) * (0.5 + Math.random() / 2);
    await new Promise((r) => setTimeout(r, delayMs));
  }
}

// Generate the key once, outside the retry loop.
const idempotencyKey = crypto.randomUUID();
const res = await withRetry(() =>
  fetch("https://api.getstarted.ae/v1/customers", {
    method: "POST",
    headers: {
      Authorization: "Bearer gs_test_xxx",
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({ name: "Northwind Labs", email: "ops@northwindlabs.com" }),
  }),
);

If 5xx errors persist, note the requestId values and contact support.