message.comDevelopers

Verify API reference Live

Two calls carry every verification: create one, then check the code the user typed back. This page documents both in full, plus the read endpoints for polling a verification's status and listing recent activity. Every field below matches the wire shape exactly, including the honest three states of dispatch.

Every machine-readable error reason, with its HTTP status and what to do about it, is a separate page: Verify API error codes. This page focuses on shapes and semantics; that one is the lookup table.

Authentication

Every endpoint on this page requires a Bearer API key with the verify scope, in the Authorization header:

Header
Authorization: Bearer mk_live_...

A missing or unrecognized key returns 401 unauthorized. A key that exists but lacks the verify scope returns 403 insufficient_scope. Keys are workspace-scoped: a key from one workspace can never read, check, or list another workspace's verifications. See Authentication for key creation and scopes.

Create a verification

POST/api/v1/verifyAuth: Bearer

Creates a verification code and dispatches it over the requested channel. Creating a verification never bills your workspace, regardless of whether the send succeeds, fails, or is never checked. Only a successful check bills, exactly once per code.

Body parameters

FieldTypeDescription
torequiredstringThe destination. An email address for the email channel, or a phone number (E.164 preferred, lightly normalized server-side) for sms. 3 to 254 characters.
channelrequiredenumOne of email, sms, voice, whatsapp, push, totp, sna. Only email and sms dispatch today; the rest return 422 channel_not_available. See setup times.
serviceIdoptionalstringA Verify Service id (vs_..., 16 alphanumeric characters after the prefix). Omit to use the workspace's default service. A workspace with no services returns 400 no_default_service.

Code samples

cURL
curl -X POST https://api.message.com/v1/verify \
  -H 'Authorization: Bearer mk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "to": "[email protected]",
    "channel": "email"
  }'
JavaScript
const res = await fetch('https://api.message.com/v1/verify', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer mk_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ to: '[email protected]', channel: 'email' }),
});
const verification = await res.json();
Python
import requests

r = requests.post(
    "https://api.message.com/v1/verify",
    headers={"Authorization": "Bearer mk_live_..."},
    json={"to": "[email protected]", "channel": "email"},
)
verification = r.json()

Response shape

201 Created
{
  "id": "1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f",
  "status": "pending",
  "channel": "email",
  "hint": "a•••@acme.com",
  "dispatch": "sent",
  "expiresAt": "2026-07-25T18:14:11.000Z"
}
  • id: the verification's identifier (UUID). Pass it to the check call.
  • status: always pending on creation. There is no other value in this field for the create response.
  • channel: echoes the requested channel.
  • hint: a masked destination, safe to render in your UI. Email hints show the first character plus domain (a•••@acme.com); phone hints show the last four digits (•••1234).
  • dispatch: what happened on the wire right now. See the dedicated section below.
  • expiresAt: ISO timestamp when the code stops being acceptable. 600 seconds (10 minutes) by default, configurable per service (60 to 900 seconds).

On a rejected request, the body is a flat { error, ...context } shape instead, for example a disabled channel:

422 Unprocessable
{
  "error": "channel_not_available",
  "channel": "voice"
}

The dispatch field

dispatch is the one field on this page that is easy to document dishonestly, so here is exactly what each value means and where it comes from:

ValueMeaning
sentThe message left our systems over the requested channel. This does not guarantee inbox or handset delivery, only that dispatch succeeded on our end.
pending_provisioningThe channel is not wired to a live send path for your workspace. In practice this does not appear today: a channel that is not live fails earlier, before dispatch, with 422 channel_not_available (see below). This value is part of the typed dispatch result and reserved for a future state where a channel is enabled but a workspace-level provisioning step (for example, a WhatsApp template pending Meta approval) has not finished. Documented as-is rather than omitted, since it is a real value the response type can carry.
errorA real send failure after the request passed every guard, for example a provider or carrier rejection. The verification row still exists and can still be checked once the underlying issue is resolved and a new code is requested, but the user never received this particular code.

Because the channel-liveness gate runs before dispatch, the only value you will observe from a live workspace today is sent on success, or a 422 channel_not_available response body (no dispatch field at all) for a non-live channel. error is reachable on a live channel if the underlying send provider rejects the message after we accept the request.

Check a code

POST/api/v1/verify/checkAuth: Bearer

Submits the code the user typed. This endpoint is dual-mode: called with a Bearer key (documented here) it is scoped to your workspace and bills $0.03 on a successful match. Called with no key at all it serves the hosted /verify screen against platform-owned rows only and is never billed; that keyless mode is not part of the authenticated API surface and is out of scope for this reference.

Body parameters

FieldTypeDescription
idrequiredstring (uuid)The verification id returned by the create call. (The hosted screen's equivalent field is named verifyId; the authenticated API accepts id.)
coderequiredstringThe code the user submitted. 4 to 8 digits, matching the service's codeLength.

Code samples

cURL
curl -X POST https://api.message.com/v1/verify/check \
  -H 'Authorization: Bearer mk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f",
    "code": "482913"
  }'
JavaScript
const res = await fetch('https://api.message.com/v1/verify/check', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer mk_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ id: verification.id, code: userSubmittedCode }),
});
const result = await res.json();
Python
check = requests.post(
    "https://api.message.com/v1/verify/check",
    headers={"Authorization": "Bearer mk_live_..."},
    json={"id": verification["id"], "code": user_submitted_code},
)
result = check.json()

Response shape

A correct code returns 200 and bills the workspace:

200 OK, match
{
  "id": "1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f",
  "verified": true
}

An incorrect or otherwise unusable code also returns 200 (not an error status), with verified: false and a machine-readable reason. This is the detail worth calling out explicitly: already_used, expired, and too_many_attempts are all 200, never 404 or 410. Only a genuinely unknown or cross-workspace id returns 404.

200 OK, no match
{
  "id": "1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f",
  "verified": false,
  "reason": "bad_code"
}

The full set of reason values, exactly when each fires, and what to do about it lives on Verify API error codes.

A wrong code does not fail the request. Check verified in the body, not the HTTP status. This is the single most common integration mistake against this endpoint.

Get a verification

GET/api/v1/verify/:idAuth: Bearer

Reads the current status of a verification without consuming an attempt. Useful for a polling UI that wants to show "code sent" or "expired" states without a check call. Scoped to the calling workspace; an unknown or cross-workspace id returns 404 not_found.

Response shape

200 OK
{
  "id": "1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f",
  "channel": "email",
  "status": "pending",
  "hint": "a•••@acme.com",
  "appName": "Acme",
  "serviceId": "vs_7hK2pQmN4rT8sX1c",
  "createdAt": "2026-07-25T18:04:11.000Z",
  "expiresAt": "2026-07-25T18:14:11.000Z",
  "consumedAt": null
}

status here reflects the row's current lifecycle state (pending, verified, expired, or failed after the attempt cap is hit), not just pending like the create response. The verification code itself is never included in this or any other response, log line, or webhook payload.

List verifications

GET/api/v1/verifyAuth: Bearer

Returns recent verifications for the workspace, newest first: a log feed for a dashboard or support tool. Not paginated beyond limit; for high-volume workspaces, narrow with serviceId and query more frequently rather than relying on an offset.

Query parameters

FieldTypeDescription
limitoptionalintegerMax rows to return. Default 50, maximum 200.
serviceIdoptionalstringNarrow the feed to one Verify Service (vs_...). An id that does not resolve in this workspace returns 404 service_not_found.
cURL
curl -X GET 'https://api.message.com/v1/verify?limit=50' \
  -H 'Authorization: Bearer mk_live_...'
200 OK
{
  "verifications": [
    {
      "id": "1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f",
      "channel": "email",
      "status": "verified",
      "hint": "a•••@acme.com",
      "appName": "Acme",
      "serviceId": "vs_7hK2pQmN4rT8sX1c",
      "createdAt": "2026-07-25T18:04:11.000Z",
      "expiresAt": "2026-07-25T18:14:11.000Z",
      "consumedAt": "2026-07-25T18:05:02.000Z"
    }
  ]
}

Rate limits

Rate limiting on Verify is layered, and each layer produces the same 429 rate_limited shape with a different scope value so you can tell which ceiling you hit:

LayerDefaultscope valueApplies to
Per-API-key request budget10 requests/second sustained, burst 50n/a (plain 429 from the request layer, before the Verify pipeline runs)Every request against any endpoint on this page, keyed by API key.
Per-service velocity, minute10 sends/minute (perMinuteCap, configurable per service)workspacePOST /v1/verify only, counted per Verify Service.
Per-service velocity, day1,000 sends/day (perDayCap, configurable per service)workspacePOST /v1/verify only, counted per Verify Service. Checked in the same guard as the per-minute cap; both share the scope: "workspace" value.
Workspace daily ceiling5,000 sends/day, across every service in the workspaceworkspace_ceilingPOST /v1/verify only. Exists so adding more services cannot multiply the per-service caps past what the workspace is trusted for.
Per-destination throttle3 sends per destination per 60 seconds, plus 10 sends per source IP per 60 secondsdestinationPOST /v1/verify only. Stops one phone number or email address, or one abusive caller IP, from being pumped with codes regardless of which service or workspace is sending.

Checks the layers run in order, top to bottom, plus a country allowlist and credit-balance gate ahead of the velocity checks; the full guard order (with every status code) is on Verify API error codes. The per-API-key request budget is infrastructure-level and applies uniformly; the other three are specific to POST /v1/verify and exist to bound send volume, not read volume, so GET /v1/verify, GET /v1/verify/:id, and POST /v1/verify/check are not subject to them (only to the per-key request budget). These layers exist to stop SMS pumping and OTP abuse; see the Verify fraud guide for the reasoning and real numbers behind each default.

Common pitfalls

  • Don't treat a 200 as success on the check call. Read verified. A wrong code, an expired code, and an already-consumed code all return 200.
  • A verification never bills on creation. Retry POST /v1/verify freely if a send appears to fail; only a successful check costs money.
  • The velocity caps are per Verify Service, not per workspace, except for the workspace daily ceiling, which is the backstop across all of them combined.
  • channel_not_available fires before country, velocity, or balance checks. A disabled-channel call never touches your credit balance or your rate-limit budget.