Skip to main content
The OTP API generates a 6 digit passcode, delivers it to a mobile number over SMS or a voice call, and verifies the code the user types back. Delivery is transactional. Marketing consent, quiet hours, Shabbat windows and frequency caps do not apply. Two gates do apply on every send: the workspace must hold an approved branded SMS sender ID, and the workspace SMS credit wallet must have enough balance. Both endpoints authenticate with the X-Sender-Tenant + X-API-Key header pair. Neither endpoint uses a session or a bearer token.
Requests are validated strictly. Any body field not listed below is rejected with 400, and only the first validation message is returned.

Send a code

Returns 200 on success, not 201.

Body

string
required
Recipient mobile number, max 32 characters. Normalised to Israeli mobile E.164 (+9725XXXXXXXX). See Phone normalisation.
string
default:"sms"
Delivery channel. One of sms or voice. A voice call reads the code aloud digit by digit.
string
The branded sender ID to send from, max 11 characters. Matched case insensitively against the workspace’s approved sender IDs. Optional in the schema but required in practice: an empty value is rejected. See Sender identity.
string
Message template, max 320 characters. Must contain the literal token {{code}}, which is replaced with the generated code. If the token is missing the platform default is used instead. Defaults to Your verification code is {{code}}.

Response

string
Opaque 21 character identifier. Pass it back to /otp/verify. The code itself is never returned.
string
The channel the code was sent on: sms or voice.
integer
Code lifetime in seconds. Always 300 (5 minutes).
integer
SMS segment count of the rendered message. This is the amount debited for a voice send. An sms send is debited in billable credits instead, which can differ. See Billing.

Send errors

Errors use the standard envelope, so a coded failure looks like this:
On a 503 the wallet hold is released, so the send costs nothing. Nothing is stored either, which means the verificationId is only ever issued after the provider has accepted the message.

Verify a code

Body

string
required
The identifier returned by /otp/send, max 64 characters.
string
required
The code the user entered, max 12 characters.

Response

boolean
true when the code matched. A match consumes the record, so the same verificationId cannot be verified twice.
string
Present only when verified is false.
A failed verification is still HTTP 200. Branch on data.verified, never on the status code. Only auth, validation and rate limiting produce a non 200 status here.

Verify errors

Code lifetime and attempts

1

Generated

A 6 digit numeric code is generated with a cryptographic random source.
2

Stored as a hash

Only an HMAC digest of the code is stored, keyed to the workspace and the verificationId. The code is never written to the response, to logs or to the database.
3

Expires after 5 minutes

The record has a 300 second lifetime. After that, verification returns expired_or_unknown.
4

At most 5 verify attempts

A verificationId accepts 5 verification attempts. The 6th returns too_many_attempts and the record is discarded. Comparison is constant time.
5

Single use

A successful verification deletes the record immediately. Re-submitting the same verificationId returns expired_or_unknown.

Phone normalisation

phone is normalised before anything else happens. Spaces, hyphens, parentheses and dots are stripped, a leading 00 is treated as +, and the result must resolve to an Israeli mobile number in E.164 form (+9725 followed by 8 digits). Anything else, including landlines and non-Israeli numbers, fails with 400 A valid mobile phone number is required before any provider call and before any wallet activity.

Sender identity

senderId is the branded sender name the SMS is sent from, and the caller ID a voice call originates from. It is resolved before the wallet is touched.
  • The value must match one of the workspace’s active sender IDs. The match is case insensitive, and the stored casing is what the recipient sees.
  • An empty value is rejected with sender_id_required. There is no fallback to a shared default.
  • The reserved platform values SENDER, INFO and SHOP are also rejected with sender_id_required. They are not available to workspace traffic.
  • An unrecognised value is rejected with sender_id_not_approved.
List the values that will be accepted with GET /public/sms-sender-ids, which returns only approved, active sender IDs. Request a new sender ID from the Senderz app; approval is not part of this API.

Billing

Every send takes a hold on the workspace SMS credit wallet before dispatch. The hold is committed once the provider accepts the message, and released if the provider rejects or fails, so a 503 costs nothing. The segments field in the response is always the segment count, so it equals the debit for voice but not necessarily for sms. Segment counting is GSM aware: 160 characters for a single GSM segment and 153 per segment beyond that, or 70 and 67 for messages containing non-GSM characters such as Hebrew or emoji. A short default-length code message is one segment and one credit. Long custom message templates cost more, so keep them tight.
Billing is measured on the rendered message, after {{code}} is substituted. For a voice call the digits are spaced out before rendering, which makes the voice body slightly longer than the SMS body for the same template.

Rate limits

Three independent limits apply to /otp/send. /otp/verify is subject to the same 60 per minute route limit and the same 1200 per minute API key limit, and has no per number cap of its own.
The per number counter increments on every authenticated send request for that number, including requests that then fail on sender ID, credits or provider delivery. Validate input and check your wallet balance before retrying, or a loop of failing sends will lock the number out for the rest of the 10 minute window.

Integration flow

1

Collect the number

Capture the mobile number and normalise it client side if you can, so an invalid number does not consume a slot against the per number cap.
2

Send

Call POST /otp/send and store the returned verificationId against the user’s session server side. Never expose it in a URL.
3

Collect the code

Prompt for the 6 digits. Allow a resend, but remember the per number cap and that each resend issues a fresh verificationId, invalidating none of the earlier ones until they expire on their own.
4

Verify

Call POST /otp/verify with the stored verificationId. Treat verified: false as a failure regardless of reason, and surface a resend affordance on expired_or_unknown and too_many_attempts.

Authentication

The header pair both endpoints require.

Errors

The shared error envelope and status codes.

Resource dropdowns

Discover the approved sender IDs you may pass.

Messages

Transactional email and SMS to a contact.