Skip to main content
These endpoints register the URLs OptimalDial calls when something happens to one of your uploads. For the receiving side — payload shape, signature verification, retry behaviour — see the Receiving webhooks guide.

The WebhookEndpoint object

FieldTypeNotes
idstring (UUID)Stable identifier.
urlstringThe HTTPS URL we deliver events to.
descriptionstring | nullFree-form, max 500 characters. Useful for distinguishing endpoints in the UI.
eventsstring[]The event types this endpoint receives. See event types.
is_activebooleanIf false, deliveries are skipped. Set to false automatically after too many failures.
verified_atstring (ISO 8601) | nullWhen we last successfully ping-verified the URL.
last_success_atstring (ISO 8601) | nullMost recent 2xx delivery.
last_failure_atstring (ISO 8601) | nullMost recent failed delivery attempt.
consecutive_failuresintegerReset to 0 on any successful delivery.
disabled_atstring (ISO 8601) | nullSet when we auto-disable after consecutive failures.
created_atstring (ISO 8601)UTC timestamp.
updated_atstring (ISO 8601)UTC timestamp of the last change.

Event types

EventFires when
upload.createdAn upload row is inserted (both API and web sources).
upload.completedProcessing finishes successfully. Payload includes signed download URLs.
upload.failedProcessing fails. Payload includes error_message.
If you don’t pass events when creating an endpoint, you get the default subset ["upload.completed", "upload.failed"]. To receive upload.created you must list it explicitly.

Create a webhook endpoint

POST /api/v1/webhooks
Registers a URL to receive events. The endpoint must respond 2xx to a synchronous ping challenge before we’ll save it — this catches typos and unreachable URLs at registration time, not at first delivery.

Request body

FieldTypeRequiredNotes
urlstring (HTTPS URL)yesMust be https:// and resolve to a public IP. See URL safety rules.
descriptionstringnoMax 500 characters.
eventsstring[]noDefaults to ["upload.completed", "upload.failed"].
curl -X POST https://api.optimaldial.com/api/v1/webhooks \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/optimaldial",
    "description": "production receiver",
    "events": ["upload.created", "upload.completed", "upload.failed"]
  }'

Response

{
  "webhook": { /* WebhookEndpoint */ },
  "secret": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
}
secret is a 64-character hex string (32 bytes of entropy). It is returned only on creation — store it next to the API key. We use it to compute the HMAC signature on every event sent to this endpoint, and your receiver uses it to verify those signatures.

Errors

StatusWhen
400URL is not HTTPS, resolves to a private IP, or didn’t respond 2xx to the ping challenge. The body includes error: "ping_failed" plus the receiver’s status code if one came back.
401Auth failure
429Rate limit hit
A 400 looks like:
{
  "detail": {
    "error": "ping_failed",
    "message": "Endpoint did not respond 2xx to the ping challenge. Verify signature, then retry.",
    "status_code": 500,
    "underlying_error": null
  }
}

List webhook endpoints

GET /api/v1/webhooks
Returns every endpoint registered for the API key’s organization, newest first.
curl https://api.optimaldial.com/api/v1/webhooks \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY"

Response

A JSON array of WebhookEndpoint objects.

Retrieve a webhook endpoint

GET /api/v1/webhooks/{webhook_id}
curl "https://api.optimaldial.com/api/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY"

Errors

StatusWhen
401Auth failure
404Endpoint doesn’t exist or belongs to another organization

Update a webhook endpoint

PATCH /api/v1/webhooks/{webhook_id}
Partial update — send only the fields you want to change. The secret is not updatable; rotate by deleting and recreating the endpoint.

Request body

FieldTypeNotes
urlstring (HTTPS URL)If changed, the new URL is ping-verified synchronously with the existing secret. The update fails if verification fails.
descriptionstringMax 500 characters.
eventsstring[]Replaces the current event list (no merge).
is_activebooleanSet to true to re-enable a manually disabled or auto-disabled endpoint. Re-enabling resets consecutive_failures to 0 and clears disabled_at.
curl -X PATCH "https://api.optimaldial.com/api/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events": ["upload.completed"]}'

Errors

StatusWhen
400New URL failed ping verification, unknown event type, empty events array
401Auth failure
404Endpoint not found
429Rate limit hit

Delete a webhook endpoint

DELETE /api/v1/webhooks/{webhook_id}
Hard delete. There is no archive — once deleted, no further events are delivered and the endpoint disappears from list calls. Existing in-flight deliveries are marked exhausted.
curl -X DELETE "https://api.optimaldial.com/api/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY"

Response

{ "status": "deleted" }

List deliveries for a webhook endpoint

GET /api/v1/webhooks/{webhook_id}/deliveries
Returns recent delivery attempts for this endpoint, newest first. Useful for debugging — you can see exactly what we sent and what your endpoint replied with for each attempt.

Query parameters

ParamTypeDefaultNotes
limitinteger (1–100)20Page size.
This endpoint is currently limit-only — there is no cursor parameter. To get older deliveries, lower the limit and rely on the natural ordering, or use the in-app developer panel.
curl "https://api.optimaldial.com/api/v1/webhooks/$WEBHOOK_ID/deliveries?limit=50" \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY"

The WebhookDelivery object

FieldTypeNotes
idstring (UUID)Delivery identifier. Echoed in the X-OptimalDial-Delivery-Id header on the actual request.
webhook_endpoint_idstring (UUID)The endpoint we tried to deliver to.
event_idstring (UUID)The event being delivered. Multiple endpoints subscribed to the same event share one event_id.
event_typestringE.g. upload.completed.
status"pending" | "in_flight" | "delivered" | "exhausted"Current state.
attempt_countintegerHow many delivery attempts we’ve made (1-indexed).
next_retry_atstring (ISO 8601)When the next attempt is scheduled, if any.
last_attempt_atstring (ISO 8601) | nullMost recent attempt time.
last_response_statusinteger | nullReceiver’s HTTP status on the most recent attempt.
last_errorstring | nullTruncated error message (e.g. timeout, DNS failure).
created_atstring (ISO 8601)When we enqueued this delivery.
delivered_atstring (ISO 8601) | nullSet when status flips to delivered.

Auto-disable

If an endpoint racks up 20 consecutive failed deliveries, we set is_active: false and stamp disabled_at. No further events go out until you re-enable it. To bring it back online: fix whatever was wrong (likely an outage or wrong URL), then PATCH the endpoint with {"is_active": true}. That call resets consecutive_failures to 0 and clears disabled_at. Future deliveries resume immediately, but anything queued during the outage that exhausted its retries is gone — re-fetch the upload state via GET /api/v1/uploads to catch up.