Notix
Use cases

Send one-time codes by email with two API calls.

To send an OTP by email from your app, call POST /v1/verify/send with the address and your app name, keep the id it returns, then call POST /v1/verify/check with that id and whatever the user typed. Notix generates the code, delivers it through the transactional email API, enforces expiry and attempt limits, rate limits each recipient and scores every request for risk. The same two calls deliver by SMS. The free plan covers it, no card needed.

The flow

Send, let the user type, check.

Your app never handles the secret. It handles an id, a form field and a yes-or-no answer.

  1. Send the code.

    One request with the recipient, your app name and, if you have it, the end user’s IP address for risk scoring. Notix answers with a verification id, when the code expires, how many wrong guesses are left and the risk result. Store the id against the user’s session.

  2. The user types what arrived.

    The email carries the digits and how long they stay valid, ten minutes by default. Your form collects the code; nothing else changes on your side.

  3. Check it.

    Post the id and the typed code. verified: true means proceed. Otherwise reason says why: wrong_code with the attempts left, expired (send a new one), already_used, too_many_attempts after the fifth wrong guess, or refused when risk scoring declined the send.

Code

The send call.

curl, or the TypeScript and Python SDKs. Every field but to has a default; the ones shown are the ones you are most likely to set.

curl
curl -X POST https://app.usenotix.dev/api/v1/verify/send \
  -H "Authorization: Bearer $NOTIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "appName": "Acme",
    "codeLength": 6,
    "expiresIn": 600,
    "clientIp": "203.0.113.10"
  }'

# 201 { "id": "ver_abc123", "status": "pending", "expiresAt": "...",
#       "attemptsRemaining": 5, "risk": { "score": 0, "level": "LOW", "reasons": [] } }

The check call.

Same id, the code the user typed, one boolean back. A failed check tells you the reason, so the form can say the right thing.

curl
curl -X POST https://app.usenotix.dev/api/v1/verify/check \
  -H "Authorization: Bearer $NOTIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "id": "ver_abc123", "code": "482913" }'

# 200 { "id": "ver_abc123", "verified": true, "status": "verified", ... }
# or  { "verified": false, "reason": "wrong_code", "attemptsRemaining": 4, ... }
Limits and policy

The numbers the API enforces for you.

These are the documented defaults and bounds. The two you set per request are code length and expiry; the rest are the same for every verification.

LimitValueNotes
Code length4 to 8 digits, default 6codeLength on the send request.
Expiry60 to 1,800 seconds, default 600expiresIn on the send request; expiresAt comes back in the response.
Wrong guesses5 per codeOnly a wrong code costs an attempt. The sixth wrong guess locks the code; attemptsRemaining is on every response.
Sends per recipient5 in 10 minutes, 30 seconds apartMore than that answers 429 RATE_LIMITED. A new send replaces the recipient's earlier pending code.
Risk scoringEvery send, before anything goes outA refused request answers 422 RISK_REFUSED with the reasons, and no code exists.
SMS channelchannel: "sms" with appNameE.164 number, prepaid from your wallet, your own sender ID once approved.

The full request and response shapes, including the risk object, are in the docs.

Design notes

Five habits that keep OTP flows safe.

Never see the code.

Notix generates the code, delivers it and checks it; your server only ever holds the verification id. Log the id, never the code, and there is no secret in your logs to leak.

Keep the expiry short.

Ten minutes is the default because a code that lives longer is a code that can be found later. A magic link or a session token is the place for a longer life, not the OTP.

Rate limit on your side too.

Notix caps sends per recipient. Add a cap per IP address and per account on your own endpoint, so a bot cannot burn a stranger's five sends by asking you to send them.

Treat RISK_REFUSED as a product decision.

A disposable domain or a suspicious IP is refused before a code exists. Ask for a different address, or route the user to a slower path; do not retry the same request in a loop.

Use SMS where phones come first.

The same two calls deliver by SMS with channel set to sms, an E.164 number and your appName. Nigeria and Kenya are routed and billed from your wallet; a sender ID of your own follows once approved.

Example

What the user receives.

The built-in template, with an app name of Acme, a six-digit code and the default ten-minute expiry. By SMS, the same facts in one line.

Email

Subject: Acme verification code: 482913

Your Acme verification code is 482913. It expires in 10 minutes. If you did not request a code, you can ignore this email.

SMS

Acme: your code is 482913. It expires in 10 minutes.

Sent from the shared Notix sender ID, or your own once the networks approve it.

FAQ

Questions, answered.

How do I send OTP codes by email from my app?
Two calls. POST /api/v1/verify/send with the recipient's address and your app name creates a code and emails it; you store the returned id. When the user types the code, POST /api/v1/verify/check with that id and the code, and read verified on the response. The code itself never passes through your servers.
What is in the email the user receives?
The built-in template sends the subject "{{appName}} verification code: {{code}}" and a short body with the code and how many minutes it stays valid. Pass a templateId to use your own Notix template; it can use {{code}}, {{appName}} and {{expiresMinutes}}. The from address defaults to no-reply@ your first verified domain.
What happens when the code expires or the user guesses wrong?
A check against an expired code answers verified: false with reason "expired"; send a fresh one. A wrong guess costs one of five attempts and returns attemptsRemaining; the sixth wrong guess locks the code with reason "too_many_attempts". A code that has already been used answers "already_used". Correct guesses never reduce the attempts.
Can I send the code by SMS instead of email?
Yes. Set channel to "sms", pass the phone number in E.164 form and your appName, and the same code arrives as a text: "Acme: your code is 482913. It expires in 10 minutes." SMS is prepaid from your wallet on every plan, and the check call is identical.
What does risk scoring do?
Before anything is sent, the request is scored for signals such as a disposable email domain or a suspicious client IP. The score, its level and every signal that fired come back on the response. A request over the threshold is refused with 422 RISK_REFUSED and no code exists, so nothing can be brute-forced.
Does the OTP API cost extra?
Email codes count as ordinary transactional emails against your plan, so the free plan's 5,000 a month covers them and no card is needed. SMS codes are charged per message from your prepaid wallet at the price shown for the destination country in Settings → Messaging.

Ship a verified sign-up this afternoon.

Two endpoints, one id in your session, and the free plan's 5,000 emails a month to test with. No card.