01Event types
Ten events. Subscribe to the ones you use.
| Event | Fires when | Payload highlights |
|---|---|---|
| message.sent | The carrier accepted the message. Billing point. | message_id, to, price, segments |
| message.delivered | The handset confirmed receipt. | delivered_at |
| message.failed | The carrier could not deliver. | reason, carrier_code |
| message.expired | Validity of 48 hours passed without delivery. | expired_at |
| message.rejected | Refused before sending. Not billed. | reason |
| campaign.started | The first message of a campaign left. | campaign_id, total |
| campaign.paused | Paused by you or by an empty balance. | campaign_id, cause |
| campaign.completed | Every message reached a final state. | sent, delivered, failed |
| balance.low | Balance crossed the threshold you set. | balance, threshold |
| balance.credited | A top-up confirmed on chain. | amount, currency, tx |
02Verify the signature
Twelve lines. Then trust the payload.
Compute HMAC-SHA256 over the timestamp, a dot and the raw body with your endpoint secret. Compare in constant time. Reject anything older than five minutes.
- Secret shown once when the endpoint is created. Rotate from the panel; both secrets are valid for 24 hours.
- Timestamp in the header and in the signed string, so a replayed request fails.
- Raw body. Verify before parsing; a re-serialised JSON will not match.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, headers, secret) {
const ts = headers["x-meteor-timestamp"];
const sig = headers["x-meteor-signature"].split("v1=")[1];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const mac = createHmac("sha256", secret)
.update(`${ts}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(mac), Buffer.from(sig));
}
import hmac, hashlib, time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
ts = headers["X-Meteor-Timestamp"]
sig = headers["X-Meteor-Signature"].split("v1=")[1]
if abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, sig)
function verify(string $raw, array $headers, string $secret): bool {
$ts = $headers['X-Meteor-Timestamp'];
$sig = explode('v1=', $headers['X-Meteor-Signature'])[1];
if (abs(time() - (int) $ts) > 300) return false;
$mac = hash_hmac('sha256', $ts . '.' . $raw, $secret);
return hash_equals($mac, $sig);
}
03Retries
Six attempts over a day. Then a replay button.
Any non-2xx or a response slower than 5 seconds counts as a failure. Delays grow between attempts so a short outage costs you nothing.
- Stable event id on every attempt. Store it and ignore duplicates.
- Undelivered events stay listed in the panel for 30 days and can be replayed one by one or in bulk.
- Endpoint health is shown per endpoint: success rate, last failure, average response time.
- 1Immediatelyfirst delivery
- 2+1 minuteafter the first failure
- 3+5 minutes
- 4+30 minutes
- 5+2 hours
- 6+12 hoursthen marked undelivered, replayable for 30 days
04Endpoint requirements
What your URL needs to be.
- Protocol
- HTTPS onlyValid certificate, TLS 1.2 or newer
- Response
- Any 2xx within 5 secondsBody ignored
- Method
- POST, JSON bodyUTF-8, unpadded
- Signature
- X-Meteor-SignatureHMAC-SHA256 over timestamp.body
- Retries
- 6 attempts over 24 hoursThen replayable for 30 days
- Endpoints
- Up to 10 per accountOwn secret and event list each
- Ordering
- Not guaranteedUse created_at and status precedence
- Source IPs
- Published, stableOptional second check
05Questions, answered
Webhook questions, answered.
Which events can I subscribe to?
message.sent, message.delivered, message.failed, message.expired and message.rejected for traffic; campaign.started, campaign.paused and campaign.completed for campaigns; balance.low and balance.credited for the account. Pick any subset per endpoint.
How are webhooks signed?
Each request carries X-Meteor-Signature, an HMAC-SHA256 of the timestamp header, a dot and the raw body, keyed with the endpoint secret shown once in the panel. Verify the signature and reject timestamps older than five minutes.
What happens if my endpoint is down?
We retry: 6 attempts over 24 hours, with growing delays. After the last attempt the event is marked undelivered in the panel and can be replayed by hand for 30 days.
Can the same event arrive twice?
Yes, on retries after a slow 2xx. Every event has a stable id; store it and ignore duplicates. Handlers should be idempotent.
How fast do I need to respond?
Within 5 seconds with any 2xx. Do the work asynchronously: queue the event, answer 200, process later.
Can I test without sending real messages?
Yes. The panel sends a test event of any type to your endpoint, and messages sent with the test API key emit real webhooks with status test.
Do you support several endpoints?
Yes. Up to ten per account, each with its own secret and event selection, for example one for production and one for staging.
Is there an IP range to allowlist?
Yes, published in the API reference and stable. Signature verification remains the recommended check; the allowlist is a second layer.
06More products
Everything else in the toolkit.
One account, one balance, one rate card. Every product below is included; none costs extra.
See the rate card