message.comDevelopers

Verify Services API Live

A Verify Service groups the settings for one use case, brand, or app: enabled channels, code length and TTL, country allowlist, send caps, SMS template variant, and custom email copy. This page covers creating, listing, updating, deleting, and previewing services, plus the webhook endpoints scoped to them. Every endpoint below accepts the same Bearer mk_ key with the verify scope you already use to send and check codes.

These endpoints also accept a dashboard admin session (JWT) with the Verify feature enabled, which is how the console's Settings pages call the same routes. That session-based mode is not documented here: this page covers the API-key-reachable surface only, since that is what your integration uses.

Authentication

Authorization: Bearer mk_live_... with the verify scope, same as the rest of the Verify API. A missing or invalid key returns 401 unauthorized; a valid key without the verify scope returns 403 insufficient_scope.

Create a service

POST/api/v1/verify/servicesAuth: Bearer

The first service created in a workspace automatically becomes the default (the service POST /v1/verify resolves to when no serviceId is given). Later services must be made default explicitly, with a PATCH.

Body parameters

FieldTypeDescription
friendlyNamerequiredstring1 to 30 characters. Appears in the default SMS body and email subject as the sender name.
enabledChannelsoptionalarray of enumAny of email, sms, voice, whatsapp, push, totp, sna. At least one entry if provided. Including a non-live channel (anything but email/sms/voice) returns 422 channel_not_available with the offending channel named. Defaults to ["email", "sms", "voice"] (the live channel set) if omitted.
codeLengthoptionalinteger4 to 8. Default 6.
codeTtlSecondsoptionalinteger60 to 900. Default 600 (10 minutes).
allowedCountriesoptionalarray of stringISO country codes, or "*" for no restriction. Default ["US", "CA"]. Ignored for the email channel.
perMinuteCapoptionalintegerMinimum 1. Default 10.
perDayCapoptionalintegerMinimum 1. Default 1000.
smsTemplateVariantoptionalenumOne of standard, short, secure. Default standard.
emailSubjectoptionalstring or nullUp to 120 characters. Overrides the default generated subject line.
emailBodyHtmloptionalstring or nullUp to 20,000 characters. Overrides the default generated email body.
notesoptionalstring or nullUp to 500 characters. Internal notes, not shown to end users.
cURL
curl -X POST https://api.message.com/v1/verify/services \
  -H 'Authorization: Bearer mk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "friendlyName": "Acme",
    "enabledChannels": ["email", "sms", "voice"],
    "codeLength": 6,
    "allowedCountries": ["US", "CA"]
  }'
JavaScript
const res = await fetch('https://api.message.com/v1/verify/services', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer mk_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ friendlyName: 'Acme' }),
});
const { service } = await res.json();
201 Created
{
  "service": {
    "id": "vs_7hK2pQmN4rT8sX1c",
    "friendlyName": "Acme",
    "isDefault": true,
    "enabledChannels": ["email", "sms", "voice"],
    "codeLength": 6,
    "codeTtlSeconds": 600,
    "allowedCountries": ["US", "CA"],
    "perMinuteCap": 10,
    "perDayCap": 1000,
    "smsTemplateVariant": "standard",
    "emailSubject": null,
    "emailBodyHtml": null,
    "notes": null,
    "createdAt": "2026-07-25T18:04:11.000Z",
    "updatedAt": "2026-07-25T18:04:11.000Z"
  }
}

List services

GET/api/v1/verify/servicesAuth: Bearer

All services in the calling workspace, no pagination.

cURL
curl -X GET https://api.message.com/v1/verify/services \
  -H 'Authorization: Bearer mk_live_...'
200 OK
{
  "services": [
    {
      "id": "vs_7hK2pQmN4rT8sX1c",
      "friendlyName": "Acme",
      "isDefault": true,
      "enabledChannels": ["email", "sms", "voice"],
      "codeLength": 6,
      "codeTtlSeconds": 600,
      "allowedCountries": ["US", "CA"],
      "perMinuteCap": 10,
      "perDayCap": 1000,
      "smsTemplateVariant": "standard",
      "emailSubject": null,
      "emailBodyHtml": null,
      "notes": null,
      "createdAt": "2026-07-25T18:04:11.000Z",
      "updatedAt": "2026-07-25T18:04:11.000Z"
    }
  ]
}

Get a service

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

Returns a single service by its vs_... id. An id that does not resolve in this workspace returns 404 service_not_found.

200 OK
{
  "service": {
    "id": "vs_7hK2pQmN4rT8sX1c",
    "friendlyName": "Acme",
    "isDefault": true,
    "enabledChannels": ["email", "sms", "voice"],
    "codeLength": 6,
    "codeTtlSeconds": 600,
    "allowedCountries": ["US", "CA"],
    "perMinuteCap": 10,
    "perDayCap": 1000,
    "smsTemplateVariant": "standard",
    "emailSubject": null,
    "emailBodyHtml": null,
    "notes": null,
    "createdAt": "2026-07-25T18:04:11.000Z",
    "updatedAt": "2026-07-25T18:04:11.000Z"
  }
}

Update a service

PATCH/api/v1/verify/services/:idAuth: Bearer

Every body field is optional; only the fields you send are changed. Adding a non-live channel to enabledChannels returns 422 channel_not_available, same as create. Unset fields keep their current value.

Additional field

FieldTypeDescription
isDefaultoptionalbooleanSet true to make this service the workspace default (swaps atomically off the previous default). Setting false on the current default returns 409 default_required: a workspace must always have exactly one default, so you make a different service default instead of un-defaulting this one.
cURL
curl -X PATCH https://api.message.com/v1/verify/services/vs_7hK2pQmN4rT8sX1c \
  -H 'Authorization: Bearer mk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "perMinuteCap": 30,
    "smsTemplateVariant": "short"
  }'
200 OK
{
  "service": {
    "id": "vs_7hK2pQmN4rT8sX1c",
    "friendlyName": "Acme",
    "isDefault": true,
    "enabledChannels": ["email", "sms", "voice"],
    "codeLength": 6,
    "codeTtlSeconds": 600,
    "allowedCountries": ["US", "CA"],
    "perMinuteCap": 10,
    "perDayCap": 1000,
    "smsTemplateVariant": "standard",
    "emailSubject": null,
    "emailBodyHtml": null,
    "notes": null,
    "createdAt": "2026-07-25T18:04:11.000Z",
    "updatedAt": "2026-07-25T18:04:11.000Z"
  }
}

Preview a template

GET/api/v1/verify/services/:id/previewAuth: Bearer

Renders the exact template a real send would produce for this service, using a sample code that respects the service's codeLength. This calls the same render functions the dispatcher uses, so the preview can never drift from a real message.

Query parameters

FieldTypeDescription
channelrequiredenumOne of the seven VerifyChannel values. Only sms, voice, and email have a template today; the rest return 422 preview_not_available.
variantoptionalenumSMS only. One of standard, short, secure. Defaults to the service's configured smsTemplateVariant.
cURL
curl -X GET 'https://api.message.com/v1/verify/services/vs_7hK2pQmN4rT8sX1c/preview?channel=sms' \
  -H 'Authorization: Bearer mk_live_...'
200 OK, SMS
{
  "channel": "sms",
  "text": "Your Acme verification code is 123456. It expires in 10 minutes."
}
200 OK, email
{
  "channel": "email",
  "subject": "123456 is your Acme verification code",
  "html": "<p>Your Acme verification code is <strong>123456</strong>...</p>",
  "text": "Your Acme verification code is 123456..."
}
422 Unprocessable, no template yet
{
  "error": "preview_not_available",
  "channel": "whatsapp"
}

Delete a service

DELETE/api/v1/verify/services/:idAuth: Bearer

Deletes a non-default service. Deleting the workspace's default service returns 409 default_service (make a different service default first, then delete this one). An id that does not resolve returns 404 not_found.

cURL
curl -X DELETE https://api.message.com/v1/verify/services/vs_7hK2pQmN4rT8sX1c \
  -H 'Authorization: Bearer mk_live_...'
200 OK
{ "deleted": true }

Service endpoint errors

StatusReasonWhereWhen
400validation_errorCreate, updateBody fails schema validation.
422channel_not_availableCreate, updateenabledChannels includes a channel that is not live. Echoes channel.
404service_not_foundGet, update, previewId does not resolve in this workspace.
404not_foundDeleteId does not resolve in this workspace. Delete uses the plain not_found reason rather than service_not_found.
409default_requiredUpdateisDefault: false sent for the current default service.
409default_serviceDeleteAttempting to delete the current default service.
422preview_not_availablePreviewRequested channel has no template today: whatsapp, push, totp, sna.

Webhook endpoints

Webhook endpoints subscribe to Verify lifecycle events (verify.started, verify.succeeded, verify.failed) instead of polling. They live on the same dual-auth surface as services and can optionally be scoped to a single Verify Service.

Create a webhook

POST/api/v1/verify/webhooksAuth: Bearer
FieldTypeDescription
urlrequiredstringMust start with https://. Validated at write time against a public-URL check (resolves DNS, rejects private/internal targets) in addition to the schema check, so a malicious endpoint cannot slip past creation.
topicsrequiredarray of stringAt least one topic, each matching verify.* or verify.<event> (for example verify.succeeded).
serviceIdoptionalstringOptional vs_... id to scope this webhook to one service. Omit to receive events from every service in the workspace. An id that does not resolve returns 404 service_not_found.
cURL
curl -X POST https://api.message.com/v1/verify/webhooks \
  -H 'Authorization: Bearer mk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://acme.com/webhooks/verify",
    "topics": ["verify.succeeded", "verify.failed"]
  }'
201 Created
{
  "webhook": {
    "id": "uuid",
    "workspaceId": "uuid",
    "url": "https://acme.com/webhooks/verify",
    "secret": "whsec_...",
    "topics": ["verify.succeeded", "verify.failed"],
    "serviceId": null,
    "enabled": true,
    "consecutiveFailures": 0,
    "disabledReason": null,
    "createdBy": "uuid",
    "createdAt": "2026-07-25T18:04:11.000Z",
    "updatedAt": "2026-07-25T18:04:11.000Z"
  }
}

The secret field is what you use to verify the X-Webhook-Signature header on incoming deliveries; see Webhooks overview for the signing scheme. It is returned on this create response and is currently also included in the list and update responses. Treat it as a credential either way: store it server-side next to your API key, and never expose it to a browser or mobile client.

List webhooks

GET/api/v1/verify/webhooksAuth: Bearer

Optional ?serviceId=vs_...narrows to one service's endpoints. Without it, every webhook in the workspace is returned.

Update a webhook

PATCH/api/v1/verify/webhooks/:idAuth: Bearer

Accepts url, topics, and enabled. Changing url re-runs the public-URL check. Setting enabled: truealso clears the endpoint's failure counter and any auto-disable reason from repeated delivery failures.

Delete a webhook

DELETE/api/v1/verify/webhooks/:idAuth: Bearer

List recent deliveries

GET/api/v1/verify/webhooks/:id/deliveriesAuth: Bearer

Returns the endpoint's last 50 delivery attempts, newest first.

Send a test delivery

POST/api/v1/verify/webhooks/:id/testAuth: Bearer

Enqueues one synthetic delivery to this endpoint with a fixed sample payload (topic verify.test), bypassing topic matching so it always reaches this endpoint regardless of its subscribed topics. Useful for confirming your receiving server and signature verification work before relying on a real event.

cURL
curl -X POST https://api.message.com/v1/verify/webhooks/{id}/test \
  -H 'Authorization: Bearer mk_live_...'
202 Accepted
{ "enqueued": true }

Webhook endpoint errors

StatusReasonWhereWhen
400validation_errorCreate, update, listBody or query fails schema validation.
400forbidden_urlCreate, update (when url changes)The URL resolves to a private, internal, or otherwise disallowed target. Response includes a reason field with the specific check that failed.
404service_not_foundCreate, list (with serviceId)serviceId does not resolve in this workspace.
404not_foundUpdate, delete, deliveries, testWebhook id does not resolve in this workspace.

Common pitfalls

  • You cannot un-default a service without defaulting another. There is always exactly one default per workspace with at least one service.
  • Preview only covers channels with a shipped template. Requesting a preview for whatsapp, push, totp, or sna returns 422, matching those channels' channel_not_available status on the send side.
  • Webhook secrets are credentials. The secret appears in create, list, and update responses today, so any code path that logs or proxies those responses is leaking a signing key. Keep it server-side only.
  • A repeatedly-failing webhook can auto-disable. If deliveries stop arriving, check enabled and disabledReason on the endpoint before assuming the server-side sender is broken.