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

EventWhen
number.order.completedAn order was filled
number.order.failedAn order could not be filled; nothing was charged
number.rental.createdA number was delivered
number.rental.renewedA number was extended
number.rental.renewal_failedAn automatic renewal could not be paid for
number.rental.cancelledA number was cancelled and refunded
number.rental.expiredA number reached the end of its rental
number.message.receivedA number received an SMS
domain.registeredA domain was registered
domain.registration_failedThe registrar refused a registration it had accepted; the hold was given back and nothing was charged
domain.renewedA domain was renewed
domain.renewal_pendingThe registrar accepted a renewal but has not finished it; the money is held until it does
domain.renewal_failedA renewal the registrar had accepted did not complete; the hold was given back
domain.expiredA domain passed its expiry date and can no longer be changed until it is renewed
domain.needs_attentionA registration or renewal has been unresolved at the registrar long enough to need a person; the money is still held
domain.updatedA domain's settings changed; changed names which of nameservers, contact, contacts, locked, suspended, privacy and customerPrice moved
domain.auth_code_generatedA 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_outAnother registrar finished taking a domain over. It is no longer renewed, charged for or changeable
domain.restoredA domain in redemption was brought back; expiresAt is its new expiry and charged what the restore cost
domain.restore_failedA restore the registrar had accepted did not complete; the hold was given back and the domain is still in redemption
domain.deletedA registration was given up at the registrar; refunded is what came back to the wallet, 0 outside the registry grace period
domain.suspendedA domain was put on hold at the registrar: it stops resolving but stays registered and still renews
domain.unsuspendedA domain was taken off hold and resolves again
licence.createdLicences were bought
licence.updatedThe seat count changed
licence.renewedThe next month was charged
licence.renewal_failedA month could not be paid for; sent once for that month
licence.cancelledLicences were cancelled, by you or after a week unpaid
price_list.updatedA price list changed; changed names the settings that moved and customerIds the customers on it
sale.below_costA sale's price was under wholesale: raised to it (raised: true) or sold at a loss
invoice.createdA customer was invoiced for what you sold them
invoice.paidYou marked an invoice paid
invoice.voidedAn 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.

In the API reference