Moorsyl Docs

Verify

Confirm a phone number belongs to your user with a one-time code.

Moorsyl Verify lets you send a one-time passcode (OTP) to a phone number and then check whether the user entered the correct code. Use it for phone verification at signup, passwordless login, or step-up authentication.

The Verify API requires a publishable key (pk_…) and can be called directly from a browser or mobile app. See API Keys.

How it works

1. Your app calls POST /api/verify/send → Moorsyl sends an SMS to the user
2. Your user types in the code they received
3. Your app calls POST /api/verify/check → Moorsyl tells you approved or denied

A verification session expires after the configured TTL (default: 10 minutes) or after the maximum number of failed attempts is exceeded.

Send a verification code

POST /api/verify/send

Sends an SMS containing a one-time code to the specified phone number and returns a verificationId you use in the next step.

Request headers

HeaderValue
x-api-keyYour publishable API key (pk_…)
Content-Typeapplication/json

Request body

FieldTypeRequiredDescription
tostringyesRecipient phone number in +222XXXXXXXXX format

Response

FieldTypeDescription
verificationIdstringUnique ID for this verification session — pass it to /verify/check

Example

Send a code
curl -X POST https://api.moorsyl.com/api/verify/send \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_..." \
  -d '{ "to": "+22236551999" }'
Response
{
  "verificationId": "ver_..."
}

Check a verification code

POST /api/verify/check

Validates the code your user entered. Returns approved on a correct match or denied when the code is wrong, expired, or already used.

Request body

FieldTypeRequiredDescription
verificationIdstringyesThe ID returned by /verify/send
codestringyesThe code the user entered

Response

FieldTypeDescription
status"approved" | "denied"Result of the check

Example

Check a code
curl -X POST https://api.moorsyl.com/api/verify/check \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_..." \
  -d '{
    "verificationId": "ver_...",
    "code": "847291"
  }'
Approved
{ "status": "approved" }
Denied
{ "status": "denied" }

Once a session is approved it cannot be checked again — it is invalidated immediately.

Get verification status

POST /api/verify/get

Retrieve the current state of a verification session by its ID. Useful server-side when you need to inspect a session without waiting for a webhook event.

Requires a secret key (sk_…).

Request body

FieldTypeRequiredDescription
verificationIdstringyesThe ID returned by /verify/send

Response

FieldTypeDescription
idstringThe verification session ID
tostringPhone number the code was sent to
status"pending" | "approved" | "expired" | "canceled"Current state of the session
attemptsnumberNumber of failed check attempts so far
expiresAtstring (ISO 8601)When the session expires
createdAtstring (ISO 8601)When the session was created
updatedAtstring (ISO 8601)When the session was last updated

Example

Get verification status
curl -X POST https://api.moorsyl.com/api/verify/get \
  -H "Content-Type: application/json" \
  -H "x-api-key: sk_live_..." \
  -d '{ "verificationId": "ver_..." }'
Response
{
  "id": "ver_...",
  "to": "+22236551999",
  "status": "pending",
  "attempts": 1,
  "expiresAt": "2024-01-01T12:10:00.000Z",
  "createdAt": "2024-01-01T12:00:00.000Z",
  "updatedAt": "2024-01-01T12:05:00.000Z"
}

Full flow example

Verify a phone number (JavaScript)
// Step 1 — send the code when the user submits their number
const { verificationId } = await fetch("https://api.moorsyl.com/api/verify/send", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": "pk_live_...",
  },
  body: JSON.stringify({ to: phoneNumber }),
}).then((r) => r.json());

// Step 2 — check the code when the user submits the OTP
const { status } = await fetch("https://api.moorsyl.com/api/verify/check", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": "pk_live_...",
  },
  body: JSON.stringify({ verificationId, code: userEnteredCode }),
}).then((r) => r.json());

if (status === "approved") {
  // Phone is verified — proceed
}

Configuration

You can customize Verify behavior per organization from the dashboard → VerifyConfiguration:

SettingDefaultDescription
OTP length6Number of digits in the code (4–8)
OTP expiry10 minHow long a code is valid before it expires
Max attempts5Failed checks before the session is invalidated
SMS templateYour verification code is {{code}}. Expires in {{expiry_minutes}} minutes.The message sent to the user — must include {{code}}
Max verifications per phone / hour5Org-level limit: how many codes can be sent to one number per hour
Max verifications per org / hour100Org-level limit: total codes sent across all numbers per hour

SMS template

The template must contain {{code}}. You may also include {{expiry_minutes}}.

Your {{code}} is valid for {{expiry_minutes}} minutes. — MyBrand

Rate limits

Verify has two independent rate limit layers:

Organization-level (configurable)

  • Per phone number per hour — prevents spamming a single number
  • Per organization per hour — protects your balance from abuse

Both limits are configurable from the dashboard and return 429 when exceeded.

IP-level (automatic, publishable keys only)

  • Applied at the Cloudflare edge before the request reaches your organization limits
  • Not configurable — exists to protect against large-scale automated abuse

Secret keys are not subject to IP-level limits.

Webhook events

Verify emits the following events you can receive via Webhooks:

EventWhen
verify.sentA code was successfully sent
verify.approvedThe user entered the correct code
verify.failedThe session expired or max attempts were reached

Errors

StatusReason
400Invalid phone number or missing required field
401Missing or invalid API key
403Publishable key not linked to an active organization
404verificationId not found or belongs to a different organization
429Rate limit exceeded (per-phone, per-org, or per-IP)

On this page