Webhooks
Receive operation, tunnel and device events at your HTTPS endpoint.
Create a webhook
Creates an endpoint and returns its signing secret once. Store it immediately. Idempotency required.
| Field | Use |
|---|---|
| url | Required public HTTPS URL. |
| events | Required non-empty array of event names. |
| description | Optional label for your team. |
{
"url": "https://example.com/webhooks/portwarp",
"events": ["command.succeeded", "command.failed", "device.offline"],
"description": "Production automation"
}List and get webhooks
Lists the key's webhook endpoints.
Returns one webhook endpoint.
Update or delete a webhook
Updates any supplied url, events, description or status. Status is enabled or disabled.
Removes the endpoint. Idempotency required.
Test and inspect deliveries
Schedules a test event and returns 202. Idempotency required.
Returns up to 100 recent delivery attempts without their event payloads.
Events
command.queued, command.claimed, command.running, command.succeeded, command.failed, command.expired, command.canceled, tunnel.started, tunnel.stopped, device.online and device.offline.
Event body
{
"id": "EVENT_ID",
"type": "command.succeeded",
"created_at": "2026-09-09T18:00:03+00:00",
"data": {"operation_id":"OPERATION_ID","device_id":"DEVICE_ID"}
}Verify the signature
Read Portwarp-Webhook-Id, Portwarp-Webhook-Timestamp and Portwarp-Webhook-Signature. Compute HMAC-SHA256 over id.timestamp.raw_body with the endpoint secret and compare it to the hexadecimal value after v1=.
signed = webhook_id + "." + timestamp + "." + raw_request_body
expected = hex(hmac_sha256(webhook_secret, signed))
valid = constant_time_equal("v1=" + expected, signature_header)Reject timestamps older than five minutes and deduplicate by event ID. The same event ID may be delivered again when Portwarp retries.
Delivery retries
Return a 2xx response quickly. Failed deliveries are retried after approximately 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours.