• 228 destinations
  • No KYC, ever
  • Bitcoin, Monero, USDT and 4 more

Webhooks

Delivery events, pushed
to your endpoint as they happen.

Register an HTTPS URL, pick the events, verify the signature. Every status change reaches you within seconds, with retries when you are down and stable ids when you are slow.

  • HMAC-SHA256 signatures
  • 6 attempts over 24 hours
  • Delivered within seconds

01Event types

Ten events. Subscribe to the ones you use.

EventFires whenPayload highlights
message.sentThe carrier accepted the message. Billing point.message_id, to, price, segments
message.deliveredThe handset confirmed receipt.delivered_at
message.failedThe carrier could not deliver.reason, carrier_code
message.expiredValidity of 48 hours passed without delivery.expired_at
message.rejectedRefused before sending. Not billed.reason
campaign.startedThe first message of a campaign left.campaign_id, total
campaign.pausedPaused by you or by an empty balance.campaign_id, cause
campaign.completedEvery message reached a final state.sent, delivered, failed
balance.lowBalance crossed the threshold you set.balance, threshold
balance.creditedA 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.
verify
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.
  1. 1Immediatelyfirst delivery
  2. 2+1 minuteafter the first failure
  3. 3+5 minutes
  4. 4+30 minutes
  5. 5+2 hours
  6. 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.