First API
Making a Request
Section titled “Making a Request”Authentication tokens are environment-specific. Send your token in the Authorization header with the Bearer prefix:
Authorization: Bearer <YOUR_API_KEY>Content-Type: application/jsonAll requests must use HTTPS and the /api namespace.
- Production:
https://api.firstdelivery.com/api/ - Sandbox:
https://sandbox.firstdelivery.com/api/
Never embed First API keys or webhook receiver secrets in URLs, logs, browser code, or published examples.
Authentication
Section titled “Authentication”- The
Bearerprefix is case-insensitive. Sending the raw token withoutBeareris still accepted for older integrations, but new integrations should sendBearer. - Tokens are only read from the
Authorizationheader. Query-string tokens are not supported. - Treat every token as an opaque string. Don’t parse it or check its length.
Existing tokens and API keys keep working
Section titled “Existing tokens and API keys keep working”All existing access tokens (UUID format) and operator API keys keep working exactly as they do today, with the same permissions. There is no forced migration and no change you need to make.
Token types
Section titled “Token types”| Prefix | What it is | Who it is for | Status |
|---|---|---|---|
f1rst_k1_<...> |
Scoped API-client key | Server-to-server integrations | Available now |
f1rst_s1_<...> |
Session token | A user who signs in with POST /api/login |
Rolling out |
| UUID | Legacy session token or operator API key | Existing integrations | Still supported |
> Rolling out: f1rst_s1_ session tokens are being issued gradually, so POST /api/login may still return a UUID session token for a while. The API already accepts both formats.
Scoped API-client keys (f1rst_k1_)
Section titled “Scoped API-client keys (f1rst_k1_)”Use these for server-to-server integrations. Each key belongs to a named API client, which is limited to the operator it was created for and the scopes you grant it.
Create a key
Section titled “Create a key”An operator admin creates keys in Dispatch → Developer Settings → API Access:
- Create an API client with a name, an optional description, and at least one scope.
- Create a key for that client. The key is shown once. Copy it immediately, because it cannot be retrieved again (First stores only a hash of it).
Admins can also manage clients through the API with an admin session:
| Method | Path | Purpose |
|---|---|---|
GET |
/api/operators/:operatorId/api-clients |
List clients, their scopes and keys |
POST |
/api/operators/:operatorId/api-clients |
Create a client. Body: name (required), description, scopes (required, at least one) |
PATCH |
/api/operators/:operatorId/api-clients/:clientId |
Update name, description, scopes or status (active / disabled) |
POST |
/api/operators/:operatorId/api-clients/:clientId/keys |
Create a key. Optional body: label, expires_at (ISO 8601, in the future). The response contains data.secret once |
DELETE |
/api/operators/:operatorId/api-clients/:clientId/keys/:keyId |
Revoke a key |
Scopes
Section titled “Scopes”A key can call only the endpoints its scopes cover. Scopes follow the folders under Platform API and Webhooks in this collection:
| Folder | Scopes |
|---|---|
| Territories | territories:read, territories:write |
| Cohorts | cohorts:read |
| Runners | runners:write |
| Merchants | merchants:read, merchants:write |
| Orders | orders:read, orders:write. Get Provider Quotes needs quotes:create |
| Strategies | strategies:read, strategies:write |
| Exports | reports:read |
| Refunds | refunds:read, refunds:write |
| Shifts | shifts:read, shifts:write |
| Webhooks | webhooks:read, webhooks:write |
Endpoints outside this collection are not available to API-client keys.
Lifetime, rotation and revocation
Section titled “Lifetime, rotation and revocation”- A key does not expire unless you set
expires_atwhen you create it. - Each API client can have at most two active keys at a time. To rotate without downtime, create a second key, deploy it, confirm that its last-used time updates, and then revoke the old key.
- Revoking a key, or disabling its API client, stops it from working.
Session tokens (f1rst_s1_)
Section titled “Session tokens (f1rst_s1_)”A session token is issued when a user signs in:
POST /api/loginContent-Type: application/json
{ "username": "<YOUR_USERNAME>", "password": "<YOUR_PASSWORD>" }The token is returned in token.id. Send it as Authorization: Bearer <token.id>. During the rollout, this value may be a UUID or an f1rst_s1_<...> token. Handle both the same way.
- A session uses the permissions of the signed-in user’s role.
- Most sessions expire after 30 days without use, or 90 days after sign-in, whichever comes first. Runner and merchant app sessions expire after 90 days without use.
POST /api/logoutwith the session’s token revokes that session and returns{ "revoked": true }.- A session stops working when its user is archived. Archived users cannot sign in.
- A failed login returns HTTP
200with{ "status": <code>, "message": "<reason>" }in the body.
Authentication errors
Section titled “Authentication errors”| Status | Body | When |
|---|---|---|
401 |
Empty | The token is missing, unknown, expired or revoked |
401 |
Access restricted to: <roles> (plain text) |
The token is valid, but the user’s role cannot call this endpoint |
403 |
{ "type": "ForbiddenError", "message": "API client requires scope: <scope>" } |
An API-client key is missing the required scope |
403 |
{ "type": "ForbiddenError", "message": "API clients cannot access this endpoint" } |
An API-client key called an endpoint outside this collection |
Webhook Contract
Section titled “Webhook Contract”First normalizes provider events into the payloads in Webhooks → Webhook Events. Production payload analysis confirms that standard order events consistently include orderId, externalId, status, status_v3, and trackerLink. Driver, location, ETA, proof-of-delivery, cancellation, fee, and provider tracking fields are conditional and must be treated as optional.
Common fields
Section titled “Common fields”| Field | Type | Behavior |
|---|---|---|
orderId |
string | First order UUID. Primary correlation key for standard order events. |
externalId |
string | Your order reference. |
status |
string | Legacy normalized status. Use with status_v3 while migrating. |
status_v3 |
string | Preferred normalized lifecycle status. Not present on Refunded. |
trackerLink |
string | First customer tracking page. |
trackerLinkFleet |
string | null | Fleet/DSP tracking page when supplied by the provider; otherwise null. |
fleetName |
string | null | Fleet/DSP display name; falls back to provider slug; null when no external provider is attached. |
dspTrackerLink |
string | null | Legacy provider-tracker field. Prefer trackerLinkFleet for new integrations. |
trackerLinkFleet and fleetName are present on every new webhook payload, including cancellations and refunds, but may be null.
Event delivery semantics
Section titled “Event delivery semantics”- Events are signals, not an authoritative event log. They may be delayed, missed, duplicated, or arrive out of order.
- First applies best-effort suppression to byte-equivalent payloads for the same operator, destination URL, and order during a short window. This reduces noise; it does not provide exactly-once delivery.
- Changed payloads, retries after failures, events outside the suppression window, and upstream provider behavior can still produce repeated lifecycle statuses.
ready_nowis an event layered on the current order status. It includesevent: "ready_now"andreadyAt; do not interpret it as a newstatus_v3value.Refundedis also an event rather than a lifecycle status and uses a provider-specific payload shape.
Webhook Consumer Best Practices
Section titled “Webhook Consumer Best Practices”- Acknowledge quickly. Validate the request minimally, enqueue it, and return a
2xxresponse. Keep processing comfortably below First’s 5-second delivery timeout. - Make processing idempotent. Use a durable key such as
(orderId, status_v3, event, relevant timestamp)or a canonical payload hash. Never rely on First’s short-window suppression as your dedupe layer. - Do not assume ordering. Compare incoming state against your stored state. Accept terminal states safely, while allowing a documented
reassignto start a new delivery lifecycle. - Treat optional fields as optional. Driver, coordinates, ETAs, tracking URLs, proof-of-delivery images, fees, and cancellation details vary by event and provider.
- Reconcile with polling. For fulfillment, customer messaging, billing, compliance, and other critical workflows, poll the order API and reconcile against webhook signals.
- Secure the receiver. Use HTTPS and configure a private authorization header in
http_options.headers. Rotate receiver credentials, redact PII from logs, and restrict replay/debug access. - Monitor failures and lag. Alert on non-2xx responses, timeouts, queue depth, stale orders, and reconciliation mismatches.
Fleet-Specific Behavior
Section titled “Fleet-Specific Behavior”Fleets differ in which order options they honor (signature, PIN, alcohol/ID check), what can be updated after an order is created, and which courier details they return. See Platform API → Orders → Create Order → Fleet-Specific Behavior for details on Uber, DoorDash, Grubhub, Just Eat, Wolt, and Relay.
> Uber drop-off PIN: For Uber orders created with options.requires_pin: true, the PIN the customer must give the courier is always the last 4 digits of customer_phone (for example, +15106809269 → 9269).
API Request Resilience
Section titled “API Request Resilience”- Retry
429and5xxresponses with exponential backoff and jitter; honorRetry-After. - Do not automatically retry most
4xxresponses without correcting the request. - Use timeouts and idempotency controls for create/update operations.
- Keep sandbox and production credentials, URLs, and data strictly separated.
Responses and Status Codes
Section titled “Responses and Status Codes”| Status | Meaning |
|---|---|
200 / 201 |
Success |
400 |
Invalid request or validation failure. Validation errors return type: "validation_error" with field_errors |
401 |
The token is missing, unknown, expired or revoked, including a token from the other environment (empty body), or the token’s role cannot call this endpoint (Access restricted to: <roles>) |
403 |
ForbiddenError: an API-client key is missing the required scope or called an endpoint closed to API clients, or the user lacks a required permission |
404 |
Resource not found |
429 |
Rate limited; back off and honor Retry-After |
5xx |
Server error; retry with bounded backoff and keep the request_id from the error body |
Platform API
Section titled “Platform API”- POST Create Territory
- GET Get Territories
- GET Get Territory
- GET Get Cohorts
- POST Create Runner
- POST Create Merchant
- PATCH Update Merchant
- GET Get Merchants
- GET Get Merchant
- POST Get Provider Quotes
- POST Create Order
- GET Get Orders
- GET Get Order
- PATCH Update Order
- POST Update Fulfillment Status
- GET Get Fulfillment Status
- POST Cancel Order
- POST Create Dispatch Strategy
- POST Replace Strategy Schedule
- GET Get Dispatch Strategies
- GET (BETA) Get Strategy Schedule
- DELETE (BETA) Remove Strategy Schedule
- PATCH Append To Strategy Schedule
- POST Create Order Export
- GET Get Order Export
- GET Download Order Export
- POST Orders (Legacy)
- POST Create DSP Refund Request
- GET Get DSP Refunds
- PATCH Update DSP Refund
- GET Get DSP Refund by DSP ID
- GET List Shifts
- GET Get Shift
- POST Create Shift
- PATCH Update Shift
- DELETE Cancel Shift
- POST Check In to Shift
- POST Check Out of Shift
Webhooks
Section titled “Webhooks”- POST Order Scheduled (scheduled)
- POST Driver Assigned (en_route_pickup)
- POST Arrived at Pickup (arrived_pickup)
- POST Picked Up (en_route_dropoff)
- POST Arrived at Dropoff (arrived_dropoff)
- POST Delivered (completed)
- POST Cancelled (cancelled)
- POST Reassigned (reassign)
- POST Refunded (Refunded)
- POST Ready Now (ready_now)
- POST Create Webhook
- GET List Webhooks
- PATCH Update Webhook
- DELETE Delete Webhook

