Documentation

esc
    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 write key 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 as localhost and names ending in .local or .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.

    Terminal
    $ 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"]}'
    JSON
    {
      "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.created or invoice.paid.
    • Every type of one resource: <resource>.*, such as instance.* or database.*. 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:

    Terminal
    $ 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:

    JSON
    {
      "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"
      }
    }
    FieldWhat it is
    idThe event's id (evt_…). The same in every delivery of this event, including retries and redeliveries: deduplicate on it.
    typeThe event type, such as instance.created.
    api_versionThe 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_atWhen it happened (RFC 3339, UTC).
    organization_idYour organization.
    project_idThe project the resource is in (acc_…).
    dataWhat 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 as create, attach or resize_storage.
    • status: succeeded or failed.
    • error: on failure only, { "code": "…", "message": "…" }. The same safe code and message the operation's failure has, fit to show your own users.
    JSON
    {
      "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

    HeaderValue
    Content-Typeapplication/json
    User-AgentPantech-Webhooks/1.0
    Pantech-Signaturet=<unix seconds>,v1=<signature>, with a second v1= for 24 hours after a secret rotation. See Verify signatures.
    Pantech-Event-IdThe event's id.
    Pantech-Event-TypeThe event's type.
    Pantech-Delivery-IdThis 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, 3xx included. 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.

    AttemptSent after the one before
    1At once
    21 minute
    35 minutes
    430 minutes
    52 hours
    65 hours
    710 hours
    810 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:

    Terminal
    $ 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 id of each event you have handled (30 days is enough) and skip one you have seen.
    • Events can arrive out of order. A retried instance.created can arrive after the instance.stopped that followed it. Use created_at to 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.

    Terminal
    $ 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_errorMeaning
    http_<status>Your URL answered with that status, such as http_500 or http_301.
    timeoutNo answer within 10 seconds.
    connection_failedThe connection was refused or dropped.
    dns_failedThe host name did not resolve.
    tls_failedThe TLS handshake failed: check the certificate and its chain.
    blocked_addressThe host name resolved to an address that is not on the public internet.
    invalid_urlThe URL is no longer valid.
    endpoint_disabled, endpoint_deletedThe 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

    Terminal
    $ 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:

    JSON
    {
      "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:

    Terminal
    $ 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:

    Terminal
    $ 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
    CommandWhat it does
    pantech webhooks list, getList 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, deleteChange a webhook (URL, events, description, enabled), or delete it
    pantech webhooks testSend a webhook.test event
    pantech webhooks rotate-secretReplace the signing secret; prints the new one once
    pantech webhooks event-typesEvery event type you can subscribe to
    pantech webhooks deliveries list, get, redeliverSee 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

    CodeStatusWhat to do
    VALIDATION_FAILED422A 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_REACHED403Your organization has 10 webhooks. Delete one first, or point one webhook at a receiver that fans out.
    FORBIDDEN403Only owners and admins can change webhooks. Use a key created by one.
    WEBHOOK_NOT_FOUND404No webhook with this id in your organization.
    WEBHOOK_DISABLED409Testing and resending need the webhook enabled. Turn it on first.
    WEBHOOK_DELIVERY_IN_PROGRESS409The delivery is being sent right now. Wait for it to finish.
    WEBHOOKS_UNAVAILABLE503Webhooks are not available yet. Try again later.

    The webhooks API reference lists every code each endpoint returns.

    Was this page helpful?

    Updated