Search

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.

  • HMAC-SHA256 signatures
  • Retries for 24 hours
  • Scoped keys
POST /hooks/numonym200 OK · 84 ms
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.

Webhook events
EventSent when
message.receivedA text arrives — with code filled in when it contains a verification code
message.statusAn outgoing text is delivered or fails
call.missedA call rings out or goes to voicemail
voicemail.readyA voicemail is recorded, with its transcript
number.renewal_dueWith each renewal reminder, from 14 days before a rental ends
number.grace_startedA rental has ended: outgoing is paused for 7 days, everything still arrives
balance.lowYour 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.

REST API endpoints
EndpointWhat it does
GET/v1/numbersYour numbers, capabilities and renewal dates
POST/v1/messagesSend a text from one of your numbers
GET/v1/messagesRead texts, within your retention setting
PUT/v1/numbers/{id}/forwardingChange where texts and calls go
POST/v1/flows/{id}/simulateRun the call flow simulator
GET/v1/balanceBalance, 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
Request access

From your account, for customers with an active number.

Questions

Developer questions

All questions & answers

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.

Your Numonym account

No account yet?
Lost your account number or password?