message.comDevelopers

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

Production base
https://app.message.com/api/v1

All 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

HTTP headers
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/json

The 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.

Paginated request
GET /api/v1/workspace/audit-log?cursor=2026-05-13T15:30:00Z|01HRX0...&limit=50
Paginated response
{
  "entries": [ /* up to 50 items */ ],
  "nextCursor": "2026-05-13T15:25:00Z|01HRX0..."
}

Rules

  • limit default is 50, maximum is 200. Above 200 returns 400 invalid_query.
  • Pass no cursor on the first request.
  • When nextCursor is null, you have reached the end of the list.
  • Cursors do not expire, but they are scoped to the exact filter combination. Changing status mid-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.

Versioned mutation
POST /api/v1/conversations/:id/assign-to-agent
Authorization: Bearer <token>
Content-Type: application/json

{ "agentId": "uuid", "expectedVersion": 4 }
409 stale_version
{
  "error": "stale_version",
  "version": 5
}

Standard retry loop

  1. GET the resource. Read version.
  2. Build the mutation, set expectedVersion: <that version>.
  3. Send it. On success, you are done.
  4. 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:

  • 2xx success.
  • 4xx your request needs to change.
  • 5xx we 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 nextCursor as 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 return 400 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.