For developers and QA
Receive SMS on a real US number, straight into your code.
A real US mobile line, not VoIP, that hands every incoming text to your code. A signed webhook fires as soon as the text is stored. A REST API lists and polls. People on your team still get the live inbox, email and Telegram. One line, $24.99 a month, cancel anytime.
Why not a number from a telecom platform?
Programmable numbers from telecom platforms are usually VoIP by classification. Twilio's own post on filtering VoIP before sending codes shows its Lookup flagging a Twilio number as nonFixedVoip (Twilio). If a service filters VoIP, its code never reaches a platform number. A Cleat line is a mobile line. That's the whole reason to use one. More on line types.
Quick start
- Get a line. Sign up and subscribe. Listing your lines works straight away; reading a line's texts answers
403until the account owner's identity is verified. - Create an API key in Settings. It starts with
clt_and is shown once. The whole contract is described in OpenAPI if you would rather generate a client. - Add a webhook endpoint in Settings, if you want pushes: a public HTTPS URL. Its signing secret starts with
whsec_and is shown once. - List your lines, then poll for texts or wait for webhooks.
curl https://cleat.so/api/v1/lines \
-H "Authorization: Bearer $CLEAT_API_KEY"{
"data": [
{
"id": "8f14e45f-ceea-4b6b-9d3c-2a1f0e7c5b10",
"phone": "13055550100",
"label": "Staging sign-ups",
"status": "active",
"createdAt": "2026-09-11T10:00:00.000Z"
}
]
}curl "https://cleat.so/api/v1/lines/$LINE_ID/messages?after=2026-09-11T10:00:00Z&limit=50" \
-H "Authorization: Bearer $CLEAT_API_KEY"{
"data": [
{
"id": "c9a7e0d2-5b1f-4e8a-9f3c-6d2b1a0e4f77",
"line": { "id": "8f14e45f-ceea-4b6b-9d3c-2a1f0e7c5b10", "phone": "13055550100", "label": "Staging sign-ups" },
"from": "32665",
"body": "704118 is your Facebook confirmation code",
"code": "704118",
"receivedAt": "2026-09-11T10:02:41.000Z",
"service": { "id": "facebook", "name": "Facebook", "color": "#0866FF" },
"contact": null,
"label": "Facebook"
}
]
}Webhooks
When a text arrives, Cleat sends a POST to each of your endpoints with this JSON body, a cleat-signature header and the user agent Cleat-Webhooks/1.0:
{
"type": "message.received",
"data": {
"id": "c9a7e0d2-5b1f-4e8a-9f3c-6d2b1a0e4f77",
"line": { "id": "8f14e45f-ceea-4b6b-9d3c-2a1f0e7c5b10", "phone": "13055550100", "label": "Staging sign-ups" },
"from": "32665",
"body": "704118 is your Facebook confirmation code",
"code": "704118",
"receivedAt": "2026-09-11T10:02:41.000Z",
"service": { "id": "facebook", "name": "Facebook", "color": "#0866FF" },
"contact": null,
"label": "Facebook"
}
}The signature header looks like t=1789120961,v1=5d41…. v1 is the hex HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the endpoint's signing secret. Check it against the raw bytes before you parse them. Reject old timestamps to block replays.
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.CLEAT_WEBHOOK_SECRET; // whsec_…
// cleat-signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">
function verified(header, body) {
const parts = Object.fromEntries(String(header).split(",").map((part) => part.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && parts.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
app.post("/cleat-webhook", express.raw({ type: "application/json" }), (req, res) => {
const body = req.body.toString("utf8");
if (!verified(req.get("cleat-signature"), body)) return res.sendStatus(401);
const { type, data } = JSON.parse(body);
if (type === "message.received") {
// data.code is Cleat's best guess at a one-time code; data.body is the full text.
console.log(data.line.phone, data.from, data.code ?? data.body);
}
res.sendStatus(200);
});
app.listen(3000);Delivery details
- Endpoints must be public
https://URLs. Cleat doesn't follow redirects. - Answer with any 2xx status within 10 seconds.
- Delivery is at-least-once. One that fails or times out is retried — five attempts in all: straight away, then after 30 seconds, 2 minutes, 10 minutes and an hour — each sending the same bytes with a fresh signature. Make your handler idempotent on
data.id; a text'sidnever changes. Settings keeps every attempt and can replay any delivery. - If all five attempts fail, catch up with the API using
?after=and thereceivedAtof the last text you handled. - Nothing is posted at all while a line is on hold or its owner hasn't verified their identity. Those texts are kept and readable the moment the reason clears, but no webhook is sent for them afterwards — read them with
?after=. - Up to five endpoints per workspace. Settings shows each endpoint's last status and how many deliveries in a row have failed.
typeismessage.received, ortestfor the sample delivery you can fire from Settings — the samedatashape, with its own ids. Answer 200 to a type you don't recognise; more may be added.- Paste a Slack incoming webhook or a Microsoft Teams workflow link instead of your own URL and that endpoint gets the chat app's own message format, with no
cleat-signature: there is no shared secret with a chat app. codeis Cleat's best guess at a one-time code, ornull. The fullbodyis always there.labelis who the text is from in words: a contact your team saved, else the service Cleat recognised from what the text says, withcontactandservicebehind it. All three arenullwhen nothing is certain. They were added later and take nothing away —from,bodyandcodeare unchanged.- A code read out over an automated call arrives the same way. The call is transcribed and delivered as a
message.received, with the transcript inbody, the number that called infrom, andcodefilled in. Nothing in the payload marks a delivery as a call rather than a text.
API reference
| Endpoint | What it returns |
|---|---|
GET /api/v1/lines | The lines in the API key's workspace, newest first: id, phone, label, status (active, grace or released) and createdAt. |
GET /api/v1/lines/{lineId}/messages | Texts on one line. after (ISO 8601) returns texts received after that moment, oldest first, for polling. before pages back, newest first. limit is 1 to 200, 50 by default. |
Authorization: Bearer clt_…. A limit outside 1–200 is clamped, not refused. Errors: 400 for a malformed timestamp, 401 for a missing or revoked key and for an expired one (code key_expired), 402 while a line is on hold, 403 until the workspace owner has verified their identity (code verify_first), 404 for a line outside the key's workspace *or* outside its scope — deliberately the same answer, 429 over the rate limit, 503 if the key's scope can't be checked.Limits and permissions
120 requests a minute per API key; past that the API answers 429, with no Retry-After header, so back off on your own. Only the workspace owner can create API keys and webhook endpoints, and a revoked key stops working immediately. A key can also be narrowed when you create it: to named lines, and to a date it stops working — which is what makes one safe to hand to an agent, a CI job or a contractor. A line outside the scope answers 404, and an expired key answers 401 with code key_expired.
Use cases that fit
- Your company's own accounts, such as cloud consoles, registrars and payment processors, with codes posted to a channel your on-call engineer watches.
- Staging and QA of your own sign-up and login flows, against a number that behaves like a customer's phone.
- Personal projects that need a US mobile number on file.
Use cases that don't
- Sending SMS. Cleat Line is receive-only: it can't send a text, place a call, or reach 911.
- Load testing or pools of numbers. Each line is a separate subscription with a verified owner. Cleat isn't built for number pools.
- Accounts that aren't yours or your company's.
- Anything that needs live voice. The API can't dial out. There is no ringing to answer, no audio to stream and no recording to fetch. You get the transcript of a call after it has ended. Cleat Full's calls are answered and placed in the web app, not through the API.
How it compares
| Telecom platform number | MailSlurp phone numbers | Cleat Line | |
|---|---|---|---|
| Number type | Often VoIP by classification; Twilio's Lookup flags a Twilio number as nonFixedVoip | “dedicated, non-VoIP mobile numbers” (MailSlurp) | Real US mobile line, not VoIP |
| Built for | Sending and receiving at scale | QA and development testing | Your own accounts, QA and small teams |
| Owner verification | Business KYC and 10DLC registration | Not stated | Government ID and selfie, or ID-only reviewed by a person |
| Getting texts into code | Webhooks and API | API and webhooks | Signed webhooks and REST API, plus Telegram, email and the inbox |
| Price | Per number plus per message | Not shown on the page | $24.99/month, incoming texts included |
Sharing codes with people who don't write code? See shared 2FA for teams. Why Google Voice and other internet numbers miss codes: Google Voice and verification codes. How we handle identity: trust and safety. Full billing rules: pricing.
Keep reading
Questions
Is there a free tier or sandbox?
No. One line is $24.99 a month, cancel anytime, and a line's texts stay locked until its owner has verified their identity.
Can I send SMS through the API?
No. Cleat Line receives; it never sends.
What if a service phones the code instead of texting it?
The call is transcribed and reaches your webhook and the API as an ordinary message, with the code extracted and the caller's number in from. No field marks it as a call.
Are there rate limits?
Yes: 120 requests a minute per API key. Webhook deliveries don't count against it.
How fast do webhooks fire?
Cleat posts to your endpoints as soon as it has stored the text, alongside the live inbox, email and Telegram. We haven't published a measured latency, so we don't quote one.
Can I get more than one number?
You can add lines to a workspace, and each one is its own $24.99 subscription. Cleat isn't built for pools of numbers.
Do you keep my messages?
Texts are kept until you delete the workspace or your account. The Privacy Policy has the details.
Sources
Facts about other companies come from their own pages, checked in September 2026.
- Twilio, filtering VoIP numbers before sending codes https://www.twilio.com/en-us/blog/filter-voip-before-otp-verification
- MailSlurp, phone numbers https://www.mailslurp.com/product/phone-number-sms-service/