Developers
SMS and call webhooks: your texts and calls, as signed JSON
Push every text, missed call and voicemail to your own server — with the verification code already extracted. A REST API with scoped keys is in private beta.
Numonym-Event: message.received
Numonym-Signature: t=1759069200,v1=5f8c…e21a
{
"id": "evt_01J8Z6Q4M9",
"type": "message.received",
"created": "2026-09-28T14:20:00Z",
"data": {
"number": "+13055550142",
"from": "+447700900418",
"text": "Your code is 482 913",
"code": "482913"
}
}
Webhooks
Events you can subscribe to
Choose the events per endpoint. Each one is a POST with a JSON body and a signature header.
| Event | Sent when |
|---|---|
| message.received | A text arrives — with code filled in when it contains a verification code |
| message.status | An outgoing text is delivered or fails |
| call.missed | A call rings out or goes to voicemail |
| voicemail.ready | A voicemail is recorded, with its transcript |
| number.renewal_due | With each renewal reminder, from 14 days before a rental ends |
| number.grace_started | A rental has ended: outgoing is paused for 7 days, everything still arrives |
| balance.low | Your balance falls under the threshold you set |
Security
Verify every request
The signature is an HMAC-SHA256 of timestamp.raw_body, keyed with the endpoint’s secret.
- Use the raw body, before any JSON parsing
- Compare in constant time
- Reject timestamps older than 5 minutes
- Ignore event ids you have already processed
import crypto from 'node:crypto';
export function verify(rawBody, header, secret) {
const p = Object.fromEntries(header.split(',').map((s) => s.split('=')));
if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(`${p.t}.${rawBody}`).digest('hex');
return p.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(p.v1), Buffer.from(expected));
}
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
p = dict(part.split("=", 1) for part in header.split(","))
if abs(time.time() - int(p.get("t", 0))) > 300:
return False
signed = p["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, p.get("v1", ""))
function verify(string $rawBody, string $header, string $secret): bool
{
parse_str(str_replace(',', '&', $header), $p);
if (abs(time() - (int) ($p['t'] ?? 0)) > 300) {
return false;
}
$expected = hash_hmac('sha256', ($p['t'] ?? '') . '.' . $rawBody, $secret);
return hash_equals($expected, (string) ($p['v1'] ?? ''));
}
REST API · private beta
Manage numbers and messages from code
Opened account by account while we stabilize the endpoints. The same spending caps apply to API calls.
| Endpoint | What it does |
|---|---|
| GET/v1/numbers | Your numbers, capabilities and renewal dates |
| POST/v1/messages | Send a text from one of your numbers |
| GET/v1/messages | Read texts, within your retention setting |
| PUT/v1/numbers/{id}/forwarding | Change where texts and calls go |
| POST/v1/flows/{id}/simulate | Run the call flow simulator |
| GET/v1/balance | Balance, top-ups and spending caps |
API keys
- Scopes: read-only, messages, numbers, call flows
- Optional IP allow-list and expiry date
- Shown once, revocable in one click
- No email or company details needed
From your account, for customers with an active number.
Is there an API today?
Webhooks are available on every number: texts, missed calls, voicemail and renewal events are pushed to your URL as signed JSON. The REST API is in private beta and opened account by account — customers open a ticket from their account with the topic “API access” and tell us what they’re building.
How do I know a webhook really comes from you?
Every request carries a Numonym-Signature header: a timestamp and an HMAC-SHA256 of the timestamp and the raw body, keyed with your endpoint’s secret. Recompute it, compare in constant time, and reject anything older than five minutes.
What happens if my server is down?
Failed deliveries are retried with exponential backoff for 24 hours. Every event has a unique id, so you can safely ignore duplicates.
Do you keep the webhook bodies?
No. The delivery log keeps the event type, the time and your server’s HTTP status for 7 days — never the message text, which follows your retention setting.
Start with a number, add the code later
Webhooks work on every number from day one.