Authentication and environments

API keys, the sandbox, idempotent retries and pagination.

Send your secret key as a Bearer token on every request. The key decides the environment: a gs_test_ key reaches the sandbox, a gs_live_ key reaches live. There is no separate sandbox URL.

cURL
curl https://api.getstarted.ae/v1/customers \
  -H "Authorization: Bearer gs_test_xxx"

A missing, malformed or revoked key returns 401 with an authentication_error. See Errors.

Keys

  • gs_test_… keys work in the sandbox. Use them for development, CI and demos.
  • gs_live_… keys work in live. Requests made with them file real incorporations with real registries, and those filings cannot be undone.

Both kinds are secret keys with full access to the environment they belong to.

Keeping keys safe

  • Call the API from your server only. Never put a key in a browser bundle, a mobile app, or any client you do not control. If your frontend needs data, proxy it through your backend.
  • Load keys from environment variables or a secrets manager. Do not commit them.
  • Rotate a key when someone who had access leaves, when it may have been logged, and on a regular schedule. Deploy the new key before revoking the old one so requests do not fail in between.
  • Use separate keys per service where you can, so a leak is contained to one place.
Important

A leaked gs_live_ key lets anyone read your customers' personal data and submit incorporations in your name. Revoke it immediately.

The sandbox

The sandbox behaves like live, with these differences:

  • Nothing is filed. No registry or compliance reviewer ever sees sandbox data. Submitting is free and safe to repeat.
  • Objects carry livemode: false. Live objects carry livemode: true. Check this in your webhook handler if the same code serves both.
  • Time is simulated. A submitted sandbox incorporation advances through the stages of its status workflow on a simulated timeline, and sends the same webhooks a live one would. Use it to test your progress tracker and event handling end to end.
  • Environments are isolated. Customers, incorporations, files and webhook endpoints created in the sandbox do not exist in live, and the reverse. Using a sandbox ID with a live key returns 404 resource_not_found, even though the ID is well formed.

Because of isolation, anything you set up in the sandbox (webhook endpoints, test customers) has to be created again in live.

Idempotency

Network failures leave you unsure whether a POST went through. Send an Idempotency-Key header with every POST, and reuse the same key when you retry. If the first request was processed, the retry returns the original response instead of creating a second customer, person or incorporation.

cURL
curl https://api.getstarted.ae/v1/customers \
  -H "Authorization: Bearer gs_test_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f0c2b8e-6a51-4c1e-9d0f-3b7e2a64c918" \
  -d '{ "name": "Northwind Labs", "email": "ops@northwindlabs.com" }'
  • Use a random value, such as a UUID v4, generated once per logical operation, not once per attempt.
  • Keys are kept for 24 hours. A retry after that is treated as a new request.
  • PATCH and PUT do not need a key. Updating intake with the same body twice leaves the same result, and putting the same file into a document slot twice is a no-op.
  • POST/incorporations/{incorporationId}/submit is idempotent by design: submitting an already-submitted incorporation returns it unchanged.

Pagination

List endpoints return a list envelope:

JSON
{
  "object": "list",
  "data": [
    {
      "id": "cus_01J9ZK4R7Q2M8X",
      "object": "customer",
      "name": "Northwind Labs",
      "email": "ops@northwindlabs.com",
      "externalReference": "acct_48213",
      "metadata": { "plan": "growth" },
      "livemode": false,
      "createdAt": "2026-09-20T08:14:22Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
  • limit sets the page size, from 1 to 100. The default is 25.
  • When hasMore is true, pass nextCursor as cursor to get the next page.
  • When hasMore is false, nextCursor is null and you have everything.

Results come back newest first. To read every customer:

curl "https://api.getstarted.ae/v1/customers?limit=100" \
  -H "Authorization: Bearer gs_test_xxx"

# then, while hasMore is true:
curl "https://api.getstarted.ae/v1/customers?limit=100&cursor=NEXT_CURSOR" \
  -H "Authorization: Bearer gs_test_xxx"

Treat cursors as opaque strings. Do not build or modify them.