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 deniedA 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/sendSends an SMS containing a one-time code to the specified phone number and returns a verificationId you use in the next step.
Request headers
| Header | Value |
|---|---|
x-api-key | Your publishable API key (pk_…) |
Content-Type | application/json |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
to | string | yes | Recipient phone number in +222XXXXXXXXX format |
Response
| Field | Type | Description |
|---|---|---|
verificationId | string | Unique ID for this verification session — pass it to /verify/check |
Example
curl -X POST https://api.moorsyl.com/api/verify/send \
-H "Content-Type: application/json" \
-H "x-api-key: pk_live_..." \
-d '{ "to": "+22236551999" }'{
"verificationId": "ver_..."
}Check a verification code
POST /api/verify/checkValidates the code your user entered. Returns approved on a correct match or denied when the code is wrong, expired, or already used.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
verificationId | string | yes | The ID returned by /verify/send |
code | string | yes | The code the user entered |
Response
| Field | Type | Description |
|---|---|---|
status | "approved" | "denied" | Result of the check |
Example
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"
}'{ "status": "approved" }{ "status": "denied" }Once a session is approved it cannot be checked again — it is invalidated immediately.
Get verification status
POST /api/verify/getRetrieve 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
| Field | Type | Required | Description |
|---|---|---|---|
verificationId | string | yes | The ID returned by /verify/send |
Response
| Field | Type | Description |
|---|---|---|
id | string | The verification session ID |
to | string | Phone number the code was sent to |
status | "pending" | "approved" | "expired" | "canceled" | Current state of the session |
attempts | number | Number of failed check attempts so far |
expiresAt | string (ISO 8601) | When the session expires |
createdAt | string (ISO 8601) | When the session was created |
updatedAt | string (ISO 8601) | When the session was last updated |
Example
curl -X POST https://api.moorsyl.com/api/verify/get \
-H "Content-Type: application/json" \
-H "x-api-key: sk_live_..." \
-d '{ "verificationId": "ver_..." }'{
"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
// 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 → Verify → Configuration:
| Setting | Default | Description |
|---|---|---|
| OTP length | 6 | Number of digits in the code (4–8) |
| OTP expiry | 10 min | How long a code is valid before it expires |
| Max attempts | 5 | Failed checks before the session is invalidated |
| SMS template | Your verification code is {{code}}. Expires in {{expiry_minutes}} minutes. | The message sent to the user — must include {{code}} |
| Max verifications per phone / hour | 5 | Org-level limit: how many codes can be sent to one number per hour |
| Max verifications per org / hour | 100 | Org-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. — MyBrandRate 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:
| Event | When |
|---|---|
verify.sent | A code was successfully sent |
verify.approved | The user entered the correct code |
verify.failed | The session expired or max attempts were reached |
Errors
| Status | Reason |
|---|---|
400 | Invalid phone number or missing required field |
401 | Missing or invalid API key |
403 | Publishable key not linked to an active organization |
404 | verificationId not found or belongs to a different organization |
429 | Rate limit exceeded (per-phone, per-org, or per-IP) |