REST API conventions
Shared rules across the message.com REST API: base URL, headers, and cursor pagination are consistent everywhere. Idempotency keys and optimistic locking are not universal yet; this page is honest about exactly where they apply.
Base URL
https://app.message.com/api/v1All endpoints listed in these docs are relative to that base. There is no separate sandbox host: test traffic uses msg_test_* API keys (planned) routed against the same base URL. We never break URL paths inside the v1 surface (see Versioning).
Required headers
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/jsonThe Authorization header carries a workspace JWT (today) or an API key (planned). The other two are standard and we reject requests missing them with 400 invalid_body rather than silently coercing.
Cursor pagination
Cursor-paginated list endpoints (the audit log is the clearest example) return a fixed slice plus a nextCursor. Pass that cursor as the cursor query parameter on the next request to fetch the next page. Cursors are opaque strings, but their format is stable: <ISO-timestamp>|<uuid> so you can decode for debugging. Not every list endpoint on this site is cursor-paginated yet; check the specific resource page.
GET /api/v1/workspace/audit-log?cursor=2026-05-13T15:30:00Z|01HRX0...&limit=50{
"entries": [ /* up to 50 items */ ],
"nextCursor": "2026-05-13T15:25:00Z|01HRX0..."
}Rules
limitdefault is 50, maximum is 200. Above 200 returns400 invalid_query.- Pass no cursor on the first request.
- When
nextCursorisnull, you have reached the end of the list. - Cursors do not expire, but they are scoped to the exact filter combination. Changing
statusmid-pagination invalidates the cursor. - We never use offset-based pagination. Skip-and-take falls apart at scale and we prefer to keep one consistent model across the API.
Idempotency
Not available yet. There is no client-facing Idempotency-Key header on outbound REST calls today, so retries after a timeout can duplicate a POST. Idempotency keys are used internally for inbound provider webhooks (email, billing, carrier delivery) but that mechanism isn't exposed to API callers. Design retries with this in mind: prefer a read-then-write pattern, and lean on optimistic locking below where it's supported.
Optimistic locking
Not a blanket convention: only specific endpoints support it. Conversation transfer/assignment (assign-to-agent, assign-to-department) and knowledge-base article updates carry a version integer and require or accept an expectedVersion body field; on mismatch they return 409 stale_version and refuse the write. Most other resources (agents, campaigns, departments, sites) have no version field and no optimistic-locking behavior: the last write wins.
POST /api/v1/conversations/:id/assign-to-agent
Authorization: Bearer <token>
Content-Type: application/json
{ "agentId": "uuid", "expectedVersion": 4 }{
"error": "stale_version",
"version": 5
}Standard retry loop
- GET the resource. Read
version. - Build the mutation, set
expectedVersion: <that version>. - Send it. On success, you are done.
- On
409 stale_version, GET again, merge your intent with the latest state, retry. Cap retries at three.
This is how the dashboard handles two agents touching the same conversation in the same second. It is also how you should write any backend service that mutates shared state. Fowler's pattern if you want the long form.
IDs
All resource IDs are UUIDs (v4 or v7). They are URL-safe, globally unique, and never reused. We do not expose numeric autoincrement IDs anywhere.
Timestamps
Every timestamp in the API is ISO 8601 UTC with milliseconds (2026-05-13T15:30:00.123Z). We never return naked epoch seconds. If you need epoch, parse the ISO string client-side.
Status codes
See Errors, rate limits, versioning for the full table. The short version:
2xxsuccess.4xxyour request needs to change.5xxwe have a problem. Retry with backoff.
Content negotiation
JSON only. We do not serve XML, msgpack, or protobuf on the public API. Upload endpoints accept multipart/form-data for binary payloads.
CORS
The API allows browser requests from any origin so long as a valid bearer token is present. For unauthenticated widget config the allowed origins list is workspace-configurable. We never echo arbitrary Origin headers without validation.
Common pitfalls
- Polling lists for new items. Use the Socket.io agent namespace instead. The list endpoints exist for backfill and audit, not for live updates.
- Treating
nextCursoras an offset. Cursors are opaque. Do not parse, do not arithmetic. - Reusing an idempotency key with a different body. That is a 409 by design, not a bug.
- Skipping
expectedVersion. Endpoints that require it return400 invalid_body; do not retry blindly. - Hardcoding
app.message.com. Read the base URL from config so we can change CDN edges without your code shipping.