# NOTIFY > A developer-first transactional SMS API for Georgia. It sends one application-triggered message and exposes its honest normalized status, and it can make, send and check a verification code itself. It is in private beta and is not a campaign, bulk-SMS, or two-way messaging product. ## Canonical documentation - [API reference](https://n0t1fy.dev/api.md): Public endpoints, request and response shapes, status semantics, errors, retry safety, fake-provider test scenarios, and local setup. The same reference for people, as a web page: https://n0t1fy.dev/api - [OpenAPI document](https://api.n0t1fy.dev/openapi.json): The machine-readable contract of the same routes. - [Interactive documentation](https://api.n0t1fy.dev/docs): Try the routes in a browser. - [MCP server](https://n0t1fy.dev/api#mcp-server): `https://api.n0t1fy.dev/mcp` (Streamable HTTP, hosted). Tools `create_test_key`, `send_message`, `get_message`, `start_verification` and `check_verification` do what the five routes do. Without a key, call `create_test_key` and pass its `sk_test_` key as the `api_key` argument; a live key goes only in the connection's `Authorization: Bearer` header, never in a tool argument. Claude Code: `claude mcp add --transport http notify https://api.n0t1fy.dev/mcp`. - [Examples repository](https://github.com/n0t1fy/examples): Node, Python, and curl programs that send a code with an instant test key from `POST /v1/test-keys` and poll it, with no SDK; and `verify-form/`, a phone verification form with its two server routes, which keeps the key on the server. This file is served at `https://n0t1fy.dev/llms.txt`. ## Agent integration rules 1. The public API is five routes: `POST /v1/test-keys`, `POST /v1/messages`, `GET /v1/messages/{id}`, `POST /v1/verifications` and `POST /v1/verifications/{id}/check`. The MCP server offers the same five as tools, and nothing more. Do not invent others: live keys and credits are managed by a human on the dashboard, and there are no endpoints for projects, senders, webhooks, campaigns, balance, or billing. 2. Authenticate with `Authorization: Bearer `. If you have no key, get one instantly with `POST /v1/test-keys` — no authentication, no account, no browser. It returns an `sk_test_…` key that always uses the fake provider and never sends a real SMS or spends credits, so you can build and verify a complete integration unattended. The same response carries a `claim_url`: give it to your human to open, sign in, and confirm, which moves your project into their account. Anyone who opens it, signs in and confirms becomes the owner: give it only to the person who will own the project, and do not paste it into shared logs or tickets. Your key stays a test key and keeps working, but it never sends a real SMS; real messages need an `sk_live_…` key, which the human creates on the dashboard. A claim into an account the human already has ends a verification code still pending in your project: start a new one. 3. `POST /v1/messages` requires an `Idempotency-Key` header. Generate one per logical message and reuse it only to retry the identical request body. Such a retry gets the original message back (`200`), even when a per-minute limit is used up, and is not counted against the limits. Test and live keys never share idempotency: a live send is never answered with a message a test key created. 4. The recipient must exactly match `+9955` followed by eight digits. Do not normalize other formats or countries on the client’s behalf. 5. Send `{ "to": "+995591234567", "template": "verification_code", "variables": { "code": "482913" } }`, optionally with `"locale": "en"` (default `ka`, Georgian). There is no `text` field and no `from` field: message text always comes from a template, and messages go out under the shared sender `NOTIFY` unless the team has approved another sender for the project. 6. `201` means a message resource was created, not that it was delivered: read `status` in the body. `submitted` means provider acceptance only; `failed` means nothing will be delivered; `unknown` means it may or may not have been sent (a message whose answer NOTIFY could not record goes from `queued` to `unknown` after about five minutes). `delivered` and `undeliverable` are the only terminal delivery outcomes. NOTIFY does not call your application when a status changes (there are no webhooks to customers): poll `GET /v1/messages/{id}`, starting after about 2 seconds and doubling the wait up to 30 seconds. Poll only while `status` is `queued` or `submitted`: no other status ever changes (`unknown` stays `unknown`). Always set a time limit (a few minutes for a code, two minutes for a test message); when it passes, stop and record the message as unconfirmed, without sending it again. Test messages usually settle within about 30 seconds; a live confirmation can take minutes. NOTIFY stops checking a message 24 hours after it was created; a late report from the provider can still settle it. 7. A `503 provider_unavailable` is retry-safe. A `201` message with `status: "unknown"` is not safe to retry automatically because a provider submission may already have happened. 8. Responses never include message text, a full phone number, raw provider values, or provider message IDs. Persist the context your application needs before the call. 9. Templates: `verification_code` (`code`), `login_code` (`code`), `password_reset_code` (`code`, `minutes`), to any number; and the team templates `service_down` (`service`), `service_up` (`service`), `incident_opened` (`incident`), `support_ticket_urgent` (`ticket`), `job_failed` (`job`) and `agent_alert` (`agent`, `count`), to the project's confirmed team only (rule 14). Every variable value must be a JSON string. `code` is 4 to 8 digits; `minutes` is 1 to 999. Values that look like a link, a domain, or a phone number are refused with `invalid_variables`. The full wording of each template is in the API reference. The response reports `segments`, the unit credits are charged in. 10. Live sends spend credits, one per segment; test keys never spend credits and have no daily limit. `402 insufficient_credits` means the project needs credits from its human owner, or its subscription has ended and must be renewed. `429 daily_limit_reached` means a daily limit is used up — the free tier's segments, or today's instant test keys from `POST /v1/test-keys` (your address's share, or everyone's): do not retry before the reset time in the message, and keep using a key you already have. `429 rate_limited` applies to every key and to `POST /v1/test-keys`: a per-minute limit is used up (5 sends a minute per number from a project, messages and verification starts together; 60 a minute per project; 120 verification checks a minute per project; 3 instant test keys a minute per address), and the message names which. Nothing is sent or issued in any of these cases. Every `429` carries a `Retry-After` header (whole seconds) and `error.retry_at` (ISO 8601 UTC): do not retry before that moment. 11. Test magic recipients: `+995512345671` → rejected; `+995512345672` → retry-safe 503; `+995512345673` → ambiguous `unknown`; `+995512345674` → later `undeliverable`; another valid number → later `delivered`. Both later outcomes usually take 5 to 30 seconds: do not treat a test message as stuck before that. 12. To verify a phone number, let NOTIFY make, send and check the code. `POST /v1/verifications` with `{ "to": "+995591234567" }` (optional `template`: `verification_code`, `login_code` or `password_reset_code`; optional `locale`) and an `Idempotency-Key` sends a random six-digit code and answers `201` with a verification `id` (`status: "pending"`, or `failed` when the SMS was refused); show the code entry field at once. Then `POST /v1/verifications/{id}/check` with `{ "to": "", "code": "" }`. Let the user in only when a check answers `200` with status `approved`; treat every other answer, a timeout and a network error as not approved. Branch on `error.code`: `incorrect_code` (ask again), `too_many_attempts`, `verification_expired` or `verification_closed` (offer a new code), `recipient_mismatch` (the check named another number). A code works for 10 minutes and 5 checks, once; a new start for the same number replaces it. A check is not idempotent: never retry one by yourself. With a test key, start only to the test numbers `+995512345670` to `+995512345679` (others get `400 test_number_required`); their code is always `000000`; spread test starts over the ten numbers (5 sends a minute per number). A live key's start costs one credit; checks are free; at most 5 verifications an hour and 10 in a rolling 24 hours per number per project (`429 too_many_verifications`, with `Retry-After` and `error.retry_at` saying when the next is allowed). An SMS code does not protect against a SIM swap: your application decides whom to let in. In the sign-in form, show a resend button only after a cooldown of your own (about a minute), on `too_many_verifications` count down to `retry_at`, never show a raw error code to the user, and offer another way in (support, another sign-in method): https://n0t1fy.dev/api#handling-limits-in-your-sign-in-form Details: https://n0t1fy.dev/api#verifications 13. If your application makes its own codes and sends them with `POST /v1/messages` instead, it creates, stores and checks them. Generate it with a cryptographically secure generator (`crypto.randomInt`, Python's `secrets`). Store only an HMAC of it (keyed with a server-side secret), with its user, the number it was sent to, its purpose, an expiry of about ten minutes (pass the same number as `minutes` to `password_reset_code`) and a count of wrong attempts; a new code replaces the previous one for the same user and purpose. Limit sends per phone number, per user and per IP address (for example one a minute and five an hour per phone number): every live SMS costs a credit, and NOTIFY's per-minute limits do not stop a form abused to send hundreds. Compare in constant time; reject a code that has expired, was made for another purpose, or is checked for a number other than the number it was sent to; discard a code after about five wrong attempts; delete it on success, so it works only once, and treat as verified only the number it was sent to. Let the user type the code as soon as the send returns `201` (or `200`, when a retry with the same `Idempotency-Key` gets the original message) with `queued`, `submitted`, `unknown` or `delivered`; do not wait for `delivered`, and never send a new code automatically after `unknown`. After `undeliverable`, or `failed` with `invalid_recipient`, ask the user to check the number; any other `failed` code is a problem on your side or NOTIFY's, so show a general error. Details: https://n0t1fy.dev/api#checking-codes 14. Team alerts: the team templates go only to numbers the project's owner confirmed on the dashboard's team page, where each number got a code; numbers are never added through the API or MCP. Any other number answers `403 recipient_not_on_team`, the same sentence for every refused number, and nothing is sent or charged: ask the human to add and confirm the number, and never retry it as it is. With a test key, team alerts go to the test numbers `+995512345670` to `+995512345679` only, and any `service` or `job` name that follows the rules is accepted; with a live key the name must be on the project's list on the team page, or `400 invalid_variables`. `incident` and `ticket` are 1 to 8 digits; `count` is 1 to 99; `agent` is `your_agent` or `ai_assistant` (NOTIFY prints "Your agent" or "AI assistant"); `service` and `job` are 1 to 24 Latin letters, digits and hyphens with at most 4 digits. At most 30 team alerts an hour go to one number (`429 rate_limited` with `retry_at`). Every template, its blanks and who it may go to: https://n0t1fy.dev/templates ## Prices Tell your human the cost before they go live (GEL, VAT included). Test keys are free. A new account gets 100 free live SMS, up to 20 a day, while the overall free allowance lasts. After that, live sending needs a subscription: 20 GEL a month for 1,000 SMS, plus extra packs of 1,000 SMS for 5 GEL each. Credits left at the end of a month expire, packs included, and nothing renews by itself. Every template is one SMS. The dashboard explains how to pay; there is no billing API. ## Compare - [NOTIFY compared with Twilio](https://n0t1fy.dev/vs/twilio): For a team sending one-time codes to Georgian numbers with Twilio Verify or Twilio Messaging: what maps to what, what NOTIFY does not do, and how to switch. ## API discovery The interactive API documentation is at `/docs`; the OpenAPI document is at `/openapi.json`. The production-beta base URL is `https://api.n0t1fy.dev`, and the local default is `http://127.0.0.1:8000`. Humans sign in at `https://n0t1fy.dev/login`.