Skip to content

For developers

Everything the Bottle app does, it does through this API. There is no private back door: the web app is a client like any other.

An API key belongs to a business. It carries the permissions it was granted and keeps working when whoever created it leaves. Use one for a partner’s system, an accounting sync, or anything running unattended.

A connection belongs to a person, through OAuth. It acts for them, within what they allowed, and dies with their membership. Use one for an assistant working on somebody’s behalf. See Connecting over MCP.

Both resolve to the same permissions, so an endpoint asks one question regardless of who is calling.

Our own driver apps sign in a third way: with the person’s email and password, like the web. Signing in at /auth/sign-in/email answers with the session in a set-auth-token header, and the app sends it back as Authorization: Bearer. It is the same session the web keeps in a cookie, so the app is that person, driver rules and all.

The API speaks JSON over HTTPS. Every tenant-scoped request needs a business: an API key names its own, and a session sends X-Tenant-Id.

Terminal window
curl https://api.bottle.example/customers \
-H "Authorization: Bearer btl_7f3a9c21_..."

Errors come back with a stable error code and a message written for a person, which the app shows as-is:

{ "error": "missing_scope", "message": "This needs the customers:write permission, ..." }

Depend on the code, not the message. Messages get rewritten, and translated.

The API reference is generated from the OpenAPI document the server itself publishes at /openapi.json, so it cannot describe an endpoint that does not exist.

A request can be lost on the way back: the write happened, but the answer never arrived. Send an Idempotency-Key header with any write, and sending the same request again is safe.

Terminal window
curl -X POST https://api.bottle.example/customers \
-H "Authorization: Bearer btl_7f3a9c21_..." \
-H "Idempotency-Key: 0192c7d4-5e1f-7a3b-9c2d-8e4f6a1b3c5d" \
-H "Content-Type: application/json" \
-d '{"tradingName": "The Anchor"}'
  • The first request with a key does the work. The same request with the same key gets the first answer back, with Idempotent-Replayed: true, and changes nothing.
  • A key is 8 to 100 letters, digits, dots, colons, dashes or underscores. A UUID made fresh for each write is ideal.
  • Keys belong to whoever sent them, in one business, and are kept for a week.
  • The same key on a different request is refused with idempotency_key_reused. One that arrives while the first is still being worked on gets idempotency_in_progress: wait a moment and send it again.
  • An answer that went wrong on our side (a 5xx) is not kept, so sending it again really does try again.

Writes that record something done in the world, a delivery or cash taken at a door, take an optional occurredAt: when it happened, as an ISO date and time with its offset. A phone out of signal sends the delivery later, and the delivery still says when it happened.

It is taken within three days back and two minutes ahead of our clock. Outside that, or left out, it happened when the request arrived.