NBN® webhooks send an update to your own system, Slack, Microsoft Teams, Discord or Google Chat whenever something happens on your NBN® internet: an order is connected, a technician appointment is booked, a speed plan changes, there is an outage, or a charge or credit is applied. This guide lists every webhook event, shows the payload and headers we send, how to check the signature, and the IP addresses webhooks come from.
New to webhooks? Start with NBN® text messages and webhooks to add one in My Account, then come back here for the technical detail.
IP addresses webhooks come from
NBN® webhooks are sent from either of our two Australian data centres. If your firewall only lets in known addresses, allow both:
| Data centre | IP address |
|---|---|
| Melbourne | 103.140.134.210 |
| Sydney | 103.137.56.120 |
- Webhooks are sent over IPv4 only.
- Either address can send any update, including a test, so allow both rather than just one.
- Every request has the user agent
Crazytel-NBN-Webhooks/1.
Your webhook address
- It must start with
https://and have a valid certificate for its host name. - It must be reachable from the internet. Private or local addresses (such as
localhost,10.x.x.xor192.168.x.x) are refused, and we check this every time we send. - Do not put a username or password in the address. Use a custom header instead (see Options and limits).
- We never follow redirects. Use the final address your server answers on.
- You can add a backup address. If the main address cannot be reached, we try the backup straight away.
Events you can choose
Choose single events, a whole group (for example nbn.order.* for every order update), or * for everything.
| Event | When it is sent |
|---|---|
Orders (nbn.order.*) | |
nbn.order.submitted | Your new connection or transfer was sent to the NBN® network. |
nbn.order.processing | The NBN® network accepted your order and is working on it. |
nbn.order.completed | Your connection is active, or your transfer to Crazytel is done. |
nbn.order.failed | The NBN® network could not complete the order. |
nbn.order.cancelled | The order was cancelled before it was completed. |
Appointments (nbn.appointment.*) | |
nbn.appointment.booked | A technician appointment was booked. |
nbn.appointment.rescheduled | A technician appointment was changed. |
Speed plans (nbn.plan.*) | |
nbn.plan.change_requested | A speed plan change was requested. |
nbn.plan.changed | The new speed plan is active. |
nbn.plan.change_failed | The speed plan could not be changed. The service stays on its old plan. |
nbn.plan.migration_scheduled | We have told you your plan will change on a set date. Includes the date, the new price and a link to the new Critical Information Summary. |
Your service (nbn.service.*) | |
nbn.service.suspended | The service was suspended. |
nbn.service.reactivated | The service was turned back on. |
nbn.service.disconnection_requested | A disconnection was requested. |
nbn.service.disconnected | The service was disconnected or permanently removed. |
nbn.service.transferred_out | The service moved to another provider. |
nbn.service.settings_updated | Service settings were updated. |
nbn.service.ip_assignment_change_requested | A change between a dynamic and a static IP address was requested. |
nbn.service.ip_assignment_changed | The IP address change is done. |
nbn.service.ip_assignment_change_failed | The IP address change could not be made. The service keeps its previous setting. |
Billing (nbn.billing.*) | |
nbn.billing.charge_applied | A charge for your NBN® service was taken from your prepaid balance. |
nbn.billing.credit_applied | A credit for your NBN® service was added. |
nbn.billing.prepaid_debit_warning | Your prepaid account is in debit and the service is being suspended. Includes the deadline and how much to top up. |
Checks (nbn.diagnostics.*) | |
nbn.diagnostics.completed | A line test or service health check finished. |
Network (nbn.network.*) | |
nbn.network.outage | The NBN® network reported an outage that may affect your service. |
nbn.network.outage_resolved | The NBN® network cleared the outage. |
| Test | |
nbn.webhook.ping | Only sent when you send a test. It is never included by *. |
You can also choose which connections a webhook covers: all of them, one connection, or a selection (by connection, by connection type such as FTTP, or by words in the connection's name or your own reference).
What we send
Each update is one HTTP POST with a JSON body (Content-Type: application/json).
Headers
| Header | What it is |
|---|---|
X-Crazytel-Event | The event, for example nbn.order.completed. |
X-Crazytel-Event-Id | The event's id. The same as id in the body, and it never changes, even when the event is sent again. |
X-Crazytel-Event-Version | 1 the first time. Goes up by one each time you ask for the event to be sent again. |
X-Crazytel-Delivery | A new id for every single attempt. Quote it if you contact support about a delivery. |
X-Crazytel-Timestamp | When this attempt was sent, in Unix seconds. |
X-Crazytel-Signature-256 | The signature, for example t=1793755800,v1=5f2b.... |
X-Crazytel-Replay | true when you asked for the event to be sent again. Left out otherwise. |
User-Agent | Crazytel-NBN-Webhooks/1 |
Any custom headers you added are sent as well.
The JSON body
Every event has the same outer layout:
| Field | What it is |
|---|---|
id | The event id. Use it to spot duplicates. |
type | The event, for example nbn.plan.changed. |
api_version | The layout version, currently 2026-10-27. |
created_at | When the event happened, in UTC (ends in Z). |
account_code | Your Crazytel account. |
sequence | A counter for the connection. A bigger number is a newer state; use it to put updates for one connection in order. |
data.service | The connection: service_uuid (our id for it), avc_id (the NBN® service id), technology (FTTP, FTTC, FTTN, FTTB, HFC or Fixed Wireless), tenure_number, service_status and address. |
data.title | A short heading, for example NBN Connection Activated. |
data.message | A plain sentence you can show to people. |
data.action, data.phase | What happened, for example new_connection and completed. Sent with most events. |
data.operation_uuid | Our id for the request this event belongs to, when there is one. |
data.billing | Charge or credit details, when the event has them (see below). |
data.test | true on a test event. Not sent on real events. |
Example: a new connection is active.
{
"account_code": "0123456789",
"api_version": "2026-10-27",
"created_at": "2026-11-04T01:30:00Z",
"data": {
"action": "new_connection",
"message": "Good news, your NBN connection is now active.",
"operation_uuid": "7d3c1a52-0b4e-4f7a-9a51-2f6a1c9e8b10",
"phase": "completed",
"service": {
"address": "1 Example Street, Sampleton QLD 4000",
"avc_id": "AVC000000000000",
"service_status": "ACTIVE",
"service_uuid": "00000000-0000-4000-8000-000000000000",
"technology": "FTTP",
"tenure_number": 1
},
"title": "NBN Connection Activated"
},
"id": "evt_00000000000000000000000000",
"sequence": 3,
"type": "nbn.order.completed"
}
We send the body on a single line with no spaces. It is shown spread out here so it is easier to read.
Billing details
nbn.billing.charge_applied and nbn.billing.credit_applied include a billing block. Amounts are in Australian dollars and include GST.
"billing": {
"entry_id": 48211,
"entry_type": "debit",
"component": "base",
"component_label": "Speed plan",
"reason": "activation_pro_rata",
"reason_label": "First part month",
"description": null,
"amount": "24.50",
"currency": "AUD",
"amounts_include_gst": true,
"period_start_date": "2026-10-15",
"period_end_date": "2026-10-31",
"settled_at": "2026-10-15T03:12:44Z"
}
entry_type is debit for a charge and credit for a credit. Use entry_id to match an update to the charge in Usage & Billing.
Extra fields on some events
nbn.plan.migration_scheduledeffective_date,new_price_inc_gst,cis_url(the new Critical Information Summary) andcampaign_uuid.nbn.billing.prepaid_debit_warningdeadline(UTC) andtop_up_needed(dollars, GST included).nbn.service.reactivated,nbn.service.disconnected- When this is about your prepaid balance,
reasonispayment_receivedornon_payment. nbn.diagnostics.completed- A
checkblock withkind(line_checkorhealth_check),labelandreference.data.messagesays how it went.
Slack, Microsoft Teams, Discord and Google Chat
When you pick a chat layout, we post one readable line instead of the full JSON, made from the title and message:
Slack, Teams, Google Chat: {"text":"NBN Connection Activated - Good news, your NBN connection is now active."}
Discord: {"content":"NBN Connection Activated - Good news, your NBN connection is now active."}
The headers and signature are the same for every layout.
Leave out the address or billing
Each webhook has two switches, both on to start with:
- Include the service address: turn off to leave
data.service.addressout. - Include billing details: turn off to leave the
billingblock out.
Check the signature
When you add a webhook we show you a signing secret once. It starts with whsec_. Save it somewhere safe: we cannot show it again. If you lose it, make a new one.
The X-Crazytel-Signature-256 header looks like t=<timestamp>,v1=<signature>. To check it:
- Read the raw body exactly as it arrived, before you parse it.
- Join the
tvalue, a full stop, and the raw body:<t>.<body>. - Work out the HMAC-SHA256 of that text with your secret, as lowercase hexadecimal.
- Accept the update if it matches any
v1value. Reject it if it matches none, or iftis more than 5 minutes from your clock.
Example: Python
import hashlib, hmac, time
def is_from_crazytel(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = [p.split("=", 1) for p in signature_header.split(",") if "=" in p]
stamp = next((v for k, v in parts if k == "t"), "")
sent = [v for k, v in parts if k == "v1"]
if not stamp.isdigit() or abs(time.time() - int(stamp)) > 300:
return False
expected = hmac.new(secret.encode(), stamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in sent)
Example: Node.js
const crypto = require('crypto');
function isFromCrazytel(rawBody, signatureHeader, secret) {
const parts = signatureHeader.split(',').map(p => p.split('='));
const stamp = (parts.find(([k]) => k === 't') || [])[1];
const sent = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
if (!stamp || Math.abs(Date.now() / 1000 - Number(stamp)) > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(`${stamp}.${rawBody}`).digest('hex');
return sent.some(s => s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}
Replying, retries and duplicates
What your server should reply
- Reply with any
2xxstatus (such as200or204) as soon as you have the update. Do slow work afterwards. - We wait up to 5 seconds to connect and 10 seconds for your reply. Slower than that counts as a failed attempt.
- A
3xxredirect,4xxor5xxreply counts as failed. A4xxfrom your main address means your server refused it, so we do not try the backup address for that attempt.
When we try again
Each update gets 6 tries to start with (you can choose 1 to 10). After a failed try we wait, then try again:
| After try | We wait about |
|---|---|
| 1 | 15 seconds |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 and later | 6 hours |
- Waits vary by up to 20% so retries do not all arrive at once.
- If your server sends a
Retry-Afterheader (for example with a429), we wait at least that long, up to 6 hours. - After the last try the update shows as Failed, gave up in the Delivery log. You can press Send again on it at any time.
- If 15 updates in a row fail, the webhook pauses for an hour. If 50 in a row fail, we switch it off and email you. Fix your server, turn the webhook back on, then send the missed updates again.
Duplicates and order
- An update can occasionally arrive more than once. Use
id(orX-Crazytel-Event-Id) to ignore one you have already handled. - Updates can arrive out of order, for example after a retry. Use
sequenceto work out which is newest for a connection, not the order they arrive in.
Send a test
You can send a test to check your address and signature code. A test is a real, signed request sent once only. It is nbn.webhook.ping unless you choose another event, has "test": true in data, and does not count towards the failed-in-a-row totals.
Options and limits
- Webhooks per account
- Up to 10.
- Custom headers
- Up to 5 per webhook, for example an API key your server expects. Values are stored encrypted and hidden in the delivery log. Names starting with
X-Crazytel-, and standard ones likeHost,Content-TypeandUser-Agent, cannot be used. - Layouts
- JSON, Slack, Microsoft Teams, Discord or Google Chat.
- Old updates
- An update that could not be started within 24 hours (for example while the webhook was paused) is cancelled rather than sent late.
- Cost
- There is no charge for NBN® webhooks.
If deliveries fail
Open the Delivery log tab and select the update. Each try shows the reply we got from your server and a plain explanation:
| Message | What to check |
|---|---|
| We could not find your server | The host name in the address is spelled correctly and has a public DNS record. |
| We could not make a secure connection | Your certificate is valid, not expired, and matches the host name. |
| No answer in time / did not reply in time | Your server is online, and replies within 10 seconds. |
| Your server refused the connection | Your server is running, and your firewall lets in 103.140.134.210 and 103.137.56.120. |
| Your server did not accept the request | A 4xx reply. Often a signature check or a missing custom header. |
| Your server asked us to slow down | A 429 reply. We try again later. |
| Your server had an error | A 5xx reply. Check your server's logs. |
| Private or blocked network address | The address points to a private network. Use a public address. |
| Your server sent a redirect | Use the final address, including the right https:// and path. |
Still stuck? Call us on 1800 272 998 or raise a ticket in the portal helpdesk, and quote the X-Crazytel-Delivery id from the log.
