Webhooks / Overview
Webhooks
Have Pantech send a signed HTTPS request to your own URL when a machine finishes creating, a change fails, an invoice is issued or credit runs low.
A webhook is a URL of yours that Pantech sends events to. Instead of polling, your system is told when a change finishes or fails, when an invoice is issued or paid, and when credit runs low. Each event is a JSON POST, signed with a secret only you and Pantech know.
Webhooks are free. An organization can have up to 10.
Before you start
- Who can do what. Owners and admins create, change, test and delete webhooks. Every member can see them and their deliveries. An API key acts as the member who created it, so a
writekey made by a member who is not an owner or admin cannot change webhooks. - A URL on the public internet. It must be
https, at most 2,048 characters, and have no user name or password in it (https://user:pass@…is refused). Addresses that are not on the public internet are refused too: private ranges (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), loopback, link-local (and so cloud metadata addresses), carrier-grade NAT (100.64.0.0/10), multicast and reserved ranges, in IPv4 and IPv6, as well aslocalhostand names ending in.localor.internal. This is checked against the address Pantech actually connects to, at every delivery, so a name that later resolves to a private address is refused then. - A receiver that checks signatures. Anyone can send a request to a public URL. Verify the signature on every delivery before you act on it.
Create a webhook
In the console: open the account menu, choose Webhooks (or Organization › Manage webhooks), then Add webhook. Enter the URL, pick the events, and copy the signing secret it shows you once. Each webhook's page lets you turn it on or off, edit it, send a test event, rotate the secret, and see every delivery with the exact body sent.
With the API: send the URL and what to subscribe to. description is a note for yourself.
$ curl https://api.pantechdynamics.com/public/v1/webhooks \ -H "Authorization: Bearer $PANTECH_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/hooks/pantech", "description": "Production alerts", "events": ["instance.*", "invoice.paid", "credit.low"]}'{
"id": "whk_06gk3x9tq7m2v8r4y1c5d0zp8w",
"url": "https://example.com/hooks/pantech",
"description": "Production alerts",
"events": ["instance.*", "invoice.paid", "credit.low"],
"enabled": true,
"disabled_reason": null,
"failing_since": null,
"created_at": "2026-10-22T09:30:00Z",
"updated_at": "2026-10-22T09:30:00Z",
"secret": "whsec_…"
}Then send a test event to check your receiver. Every endpoint, with its fields and errors, is in the webhooks API reference.
Choose events
events takes 1 to 100 entries, each one of:
- An exact type, such as
instance.createdorinvoice.paid. - Every type of one resource:
<resource>.*, such asinstance.*ordatabase.*. A type added to that resource later is included. - Everything:
*.
Infrastructure events are named for how the change ended: <resource>.<past tense> when it finished (volume.attached), and <resource>.<verb>_failed when it failed (volume.attach_failed). Billing events have their own names (invoice.issued, credit.low). Event types lists every one, and so does the API:
$ curl https://api.pantechdynamics.com/public/v1/webhook-event-types \ -H "Authorization: Bearer $PANTECH_API_KEY"What is sent
Every delivery is a POST with a JSON body, the same shape for every event:
{
"id": "evt_06gk3x9tq7m2v8r4y1c5d0zp8w",
"type": "instance.created",
"api_version": "2026-10-22",
"created_at": "2026-10-22T09:31:12Z",
"organization_id": "org_06gk3x9tq7m2v8r4y1c5d0zp8w",
"project_id": "acc_06gk3x9tq7m2v8r4y1c5d0zp8w",
"data": {
"resource_type": "instance",
"resource_id": "vm_06gk3x9tq7m2v8r4y1c5d0zp8w",
"operation_id": "op_06gk3x9tq7m2v8r4y1c5d0zp8w",
"action": "create",
"status": "succeeded"
}
}| Field | What it is |
|---|---|
id | The event's id (evt_…). The same in every delivery of this event, including retries and redeliveries: deduplicate on it. |
type | The event type, such as instance.created. |
api_version | The shape of this body, as a date. It is 2026-10-22. A change that would break a receiver comes with a new version. |
created_at | When it happened (RFC 3339, UTC). |
organization_id | Your organization. |
project_id | The project the resource is in (acc_…). |
data | What happened. Its fields depend on type. |
Infrastructure events
data names the resource and the change, and nothing more:
resource_type:instance,volume,database, and so on.resource_id: its id. Read it through the API (GET /instances/{instance_id}) for its current state.operation_id: the operation that made the change.action: the verb that was asked for, such ascreate,attachorresize_storage.status:succeededorfailed.error: on failure only,{ "code": "…", "message": "…" }. The same safe code and message the operation'sfailurehas, fit to show your own users.
{
"resource_type": "volume",
"resource_id": "vol_06gk3x9tq7m2v8r4y1c5d0zp8w",
"operation_id": "op_06gk3x9tq7m2v8r4y1c5d0zp8w",
"action": "attach",
"status": "failed",
"error": {
"code": "PROVISIONING_RESOURCE_BUSY",
"message": "The resource was busy with another change. Wait for it to settle, then try again."
}
}Billing events
data carries the facts of the event, with amounts as integers in minor units (kobo for NGN, cents for USD) and a currency. For invoice.paid, amount_minor: 1504000 with currency: "NGN" is NGN 15,040.00. Event types lists each event's fields.
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Pantech-Webhooks/1.0 |
Pantech-Signature | t=<unix seconds>,v1=<signature>, with a second v1= for 24 hours after a secret rotation. See Verify signatures. |
Pantech-Event-Id | The event's id. |
Pantech-Event-Type | The event's type. |
Pantech-Delivery-Id | This delivery (whd_…). One event sent to two of your webhooks is two deliveries with the same event id. |
Answer quickly
A delivery succeeds when your URL answers any 2xx status within 10 seconds. Anything else is a failure and is retried:
- Any other status,
3xxincluded. Redirects are not followed: give the final URL. - No answer within 10 seconds, a refused connection, a name that does not resolve, or a TLS error. Your certificate must be valid for the host name.
So verify the signature, store the event, answer 200 or 204, and do the work afterwards, in a queue or a background job. The body you answer with is not used, except that its first 1,024 characters are kept on the delivery to help you debug.
Retries
A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours: eight attempts in all, over about 28 hours. After the eighth it is marked failed, and you can still send it again.
| Attempt | Sent after the one before |
|---|---|
| 1 | At once |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 5 hours |
| 7 | 10 hours |
| 8 | 10 hours |
A webhook that keeps failing is turned off
When every delivery to a webhook has failed for three days without a single success, the webhook is disabled: enabled becomes false and disabled_reason is failing. Deliveries still waiting are cancelled. failing_since on the webhook shows when the failures started, so you can see trouble coming.
Fix the receiver, then turn the webhook back on. That clears its failure history and gives it a fresh three days:
$ curl -X PATCH https://api.pantechdynamics.com/public/v1/webhooks/$WEBHOOK_ID \ -H "Authorization: Bearer $PANTECH_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"enabled": true}'Setting enabled to false yourself pauses a webhook the same way, with disabled_reason: "disabled_by_user". Events that happen while a webhook is disabled are not sent to it later.
Duplicates and order
Delivery is at least once, and not in order:
- The same event can arrive more than once, for example when your receiver handled it but its answer was lost. Keep the
idof each event you have handled (30 days is enough) and skip one you have seen. - Events can arrive out of order. A retried
instance.createdcan arrive after theinstance.stoppedthat followed it. Usecreated_atto order them, or treat an event as a cue and read the resource's current state from the API.
See and resend deliveries
Every event sent to a webhook, or waiting to be, is a delivery, kept for 30 days. The list is newest first, with each one's status (pending, succeeded, failed or canceled), attempts, and how the last attempt went: last_status_code, last_error, last_duration_ms and the start of last_response.
$ curl https://api.pantechdynamics.com/public/v1/webhooks/$WEBHOOK_ID/deliveries \ -H "Authorization: Bearer $PANTECH_API_KEY"last_error says why an attempt failed:
last_error | Meaning |
|---|---|
http_<status> | Your URL answered with that status, such as http_500 or http_301. |
timeout | No answer within 10 seconds. |
connection_failed | The connection was refused or dropped. |
dns_failed | The host name did not resolve. |
tls_failed | The TLS handshake failed: check the certificate and its chain. |
blocked_address | The host name resolved to an address that is not on the public internet. |
invalid_url | The URL is no longer valid. |
endpoint_disabled, endpoint_deleted | The webhook was disabled or deleted before the delivery was sent. |
GET /webhooks/{webhook_id}/deliveries/{delivery_id} returns one delivery with payload, the exact body sent. To send a delivery that succeeded, failed or was cancelled again, POST /webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver: it starts again from the first attempt, with the same event id, so a receiver that deduplicates is safe. The webhook must be enabled, and a delivery still being attempted answers 409 WEBHOOK_DELIVERY_IN_PROGRESS.
Test it
$ curl -X POST https://api.pantechdynamics.com/public/v1/webhooks/$WEBHOOK_ID/test \ -H "Authorization: Bearer $PANTECH_API_KEY" \ -H "Idempotency-Key: $(uuidgen)"This queues a webhook.test event to the webhook, whatever it subscribes to, and returns the delivery. It is sent within a few seconds, signed and retried like any other:
{
"id": "evt_…",
"type": "webhook.test",
"api_version": "2026-10-22",
"created_at": "2026-10-22T09:32:00Z",
"organization_id": "org_…",
"project_id": "acc_…",
"data": { "endpoint_id": "whk_…", "message": "This is a test event from Pantech." }
}Read the delivery a few seconds later to see how your receiver answered.
Rotate the secret
If the secret may have leaked, or you rotate secrets on a schedule, ask for a new one:
$ curl -X POST https://api.pantechdynamics.com/public/v1/webhooks/$WEBHOOK_ID/rotate-secret \ -H "Authorization: Bearer $PANTECH_API_KEY" \ -H "Idempotency-Key: $(uuidgen)"The response has the new secret, shown this once. For the next 24 hours every delivery is signed with both the new and the old secret, as two v1 values, so you can update your receiver without dropping an event. A receiver that accepts any matching v1 keeps working throughout, whichever secret it holds. After 24 hours only the new secret signs.
Rotating again within the 24 hours starts a new overlap: only the latest secret and the one just before it sign.
Change or delete a webhook
PATCH /webhooks/{webhook_id} changes url, description, events or enabled. What you leave out stays as it is, and events replaces the whole list. DELETE /webhooks/{webhook_id} deletes the webhook and its deliveries; nothing more is sent to it.
From the CLI and Terraform
The Pantech CLI manages webhooks with pantech webhooks:
$ pantech webhooks create --url https://example.com/hooks/pantech \ --event 'instance.*' --event invoice.paid --description "Production alerts"$ pantech webhooks test whk_06gk3x9tq7m2v8r4y1c5d0zp8w$ pantech webhooks deliveries list whk_06gk3x9tq7m2v8r4y1c5d0zp8w| Command | What it does |
|---|---|
pantech webhooks list, get | List your webhooks, or show one |
pantech webhooks create --url URL --event TYPE [--description TEXT] | Create a webhook; repeat --event for each type, <resource>.* or *. Prints the secret once |
pantech webhooks update, delete | Change a webhook (URL, events, description, enabled), or delete it |
pantech webhooks test | Send a webhook.test event |
pantech webhooks rotate-secret | Replace the signing secret; prints the new one once |
pantech webhooks event-types | Every event type you can subscribe to |
pantech webhooks deliveries list, get, redeliver | See deliveries, one with its payload, or send one again |
Run pantech webhooks --help for every flag.
With Terraform, a webhook is the pantechdynamics_webhook resource, and the pantechdynamics_webhook_event_types data source lists the event types. The signing secret is a sensitive attribute of the resource, so keep your state somewhere private.
Errors
| Code | Status | What to do |
|---|---|---|
VALIDATION_FAILED | 422 | A field is invalid; errors names it. INVALID_WEBHOOK_URL: give an https URL on the public internet, with no user name or password. INVALID_WEBHOOK_EVENTS: give 1 to 100 known types, <resource>.* or *. |
WEBHOOK_LIMIT_REACHED | 403 | Your organization has 10 webhooks. Delete one first, or point one webhook at a receiver that fans out. |
FORBIDDEN | 403 | Only owners and admins can change webhooks. Use a key created by one. |
WEBHOOK_NOT_FOUND | 404 | No webhook with this id in your organization. |
WEBHOOK_DISABLED | 409 | Testing and resending need the webhook enabled. Turn it on first. |
WEBHOOK_DELIVERY_IN_PROGRESS | 409 | The delivery is being sent right now. Wait for it to finish. |
WEBHOOKS_UNAVAILABLE | 503 | Webhooks are not available yet. Try again later. |
The webhooks API reference lists every code each endpoint returns.