Webhooks
Receive signed events on your own endpoint, following the Standard Webhooks specification.
A webhook endpoint is a URL of yours that ResellerHQ posts to when something happens. Deliveries follow the Standard Webhooks specification.
Add an endpoint
In the dashboard, open Developers and choose New endpoint. Or with the API:
curl -X POST https://resellerhq.com/api/v1/webhook-endpoints \
-H "Authorization: Bearer $RHQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/hooks/resellerhq","events":["number.message.received"]}'The response includes the endpoint's signing secret, which starts with whsec_. It is shown
once. Use ["*"] to receive every event.
The URL must be https and must resolve to a public address.
Events
| Event | When |
|---|---|
number.order.completed | An order was filled |
number.order.failed | An order could not be filled; nothing was charged |
number.rental.created | A number was delivered |
number.rental.renewed | A number was extended |
number.rental.renewal_failed | An automatic renewal could not be paid for |
number.rental.cancelled | A number was cancelled and refunded |
number.rental.expired | A number reached the end of its rental |
number.message.received | A number received an SMS |
domain.registered | A domain was registered |
domain.registration_failed | The registrar refused a registration it had accepted; the hold was given back and nothing was charged |
domain.renewed | A domain was renewed |
domain.renewal_pending | The registrar accepted a renewal but has not finished it; the money is held until it does |
domain.renewal_failed | A renewal the registrar had accepted did not complete; the hold was given back |
domain.expired | A domain passed its expiry date and can no longer be changed until it is renewed |
domain.needs_attention | A registration or renewal has been unresolved at the registrar long enough to need a person; the money is still held |
domain.updated | A domain's settings changed; changed names which of nameservers, contact, contacts, locked, suspended, privacy and customerPrice moved |
domain.auth_code_generated | A transfer code was issued for a domain, so it can be moved to another registrar; via says whether an API key or the dashboard asked. The code itself is never in the event |
domain.transferred_out | Another registrar finished taking a domain over. It is no longer renewed, charged for or changeable |
domain.restored | A domain in redemption was brought back; expiresAt is its new expiry and charged what the restore cost |
domain.restore_failed | A restore the registrar had accepted did not complete; the hold was given back and the domain is still in redemption |
domain.deleted | A registration was given up at the registrar; refunded is what came back to the wallet, 0 outside the registry grace period |
domain.suspended | A domain was put on hold at the registrar: it stops resolving but stays registered and still renews |
domain.unsuspended | A domain was taken off hold and resolves again |
licence.created | Licences were bought |
licence.updated | The seat count changed |
licence.renewed | The next month was charged |
licence.renewal_failed | A month could not be paid for; sent once for that month |
licence.cancelled | Licences were cancelled, by you or after a week unpaid |
price_list.updated | A price list changed; changed names the settings that moved and customerIds the customers on it |
sale.below_cost | A sale's price was under wholesale: raised to it (raised: true) or sold at a loss |
invoice.created | A customer was invoiced for what you sold them |
invoice.paid | You marked an invoice paid |
invoice.voided | An invoice was voided, by you or because a number on it was cancelled and refunded; its sales can be invoiced again |
What you receive
{
"id": "evt_01JABCDEF...",
"type": "number.message.received",
"timestamp": "2026-10-07T11:04:31.454Z",
"data": {
"messageId": "msg_01JABCDEF...",
"rentalId": "num_01JABCDEF...",
"number": "15550100002",
"sender": "WhatsApp",
"body": "Your code is 428-113",
"receivedAt": "2026-10-07T11:04:31.000Z"
}
}Three headers come with it: webhook-id, webhook-timestamp and webhook-signature.
Verify the signature
Always verify before trusting a delivery. Any Standard Webhooks library does it:
import { Webhook } from 'standardwebhooks';
const webhook = new Webhook(process.env.RHQ_WEBHOOK_SECRET);
export async function POST(request: Request) {
const body = await request.text(); // the raw body, not parsed JSON
let event;
try {
event = webhook.verify(body, Object.fromEntries(request.headers));
} catch {
return new Response('bad signature', { status: 400 });
}
// handle the event
return new Response(null, { status: 204 });
}The signature is an HMAC-SHA256 over webhook-id.webhook-timestamp.body. Libraries also
reject timestamps that are too old, which stops replays.
Retries
Answer with any 2xx within 10 seconds. Anything else is retried after 5 seconds, 5 minutes,
30 minutes, 2 hours, 5 hours, 10 hours and 14 hours, and then given up on.
Because of retries, the same event can arrive more than once. webhook-id is the same each
time, so store the ids you have handled and skip repeats.