Search Documentation
Search across all documentation pages
Webhooks

Webhooks API

The WebhookService manages signed HTTPS endpoints, the event ledger, and delivery history. All requests are scoped to a single app via your app-scoped API key (ak_live_… or ak_test_…).

See the Webhooks concept guide for the event catalog, payload shapes, signature algorithm, and verification snippets, and the Webhook Integration guide for an end-to-end SDK walkthrough.

Service base path: /transcodely.v1.WebhookService/<Method> — Connect-RPC, JSON, snake_case wire format, lowercase enum strings.


SDK methods

Every RPC below is exposed by the official SDKs. Endpoint operations live on the WebhookEndpoints resource; the event ledger lives on Events.

RPCGoPythonJavaScript
CreateWebhookEndpointclient.WebhookEndpoints.Createclient.webhook_endpoints.createclient.webhookEndpoints.create
RetrieveWebhookEndpointclient.WebhookEndpoints.Retrieveclient.webhook_endpoints.retrieveclient.webhookEndpoints.retrieve
UpdateWebhookEndpointclient.WebhookEndpoints.Updateclient.webhook_endpoints.updateclient.webhookEndpoints.update
DeleteWebhookEndpointclient.WebhookEndpoints.Deleteclient.webhook_endpoints.deleteclient.webhookEndpoints.delete
ListWebhookEndpointsclient.WebhookEndpoints.Listclient.webhook_endpoints.listclient.webhookEndpoints.list
RotateWebhookSecretclient.WebhookEndpoints.RotateSecretclient.webhook_endpoints.rotate_secretclient.webhookEndpoints.rotateSecret
SendTestWebhookclient.WebhookEndpoints.SendTestclient.webhook_endpoints.send_testclient.webhookEndpoints.sendTest
ListWebhookDeliveriesclient.WebhookEndpoints.ListDeliveriesclient.webhook_endpoints.list_deliveriesclient.webhookEndpoints.listDeliveries
GetEndpointHealthclient.WebhookEndpoints.GetHealthclient.webhook_endpoints.get_healthclient.webhookEndpoints.getHealth
ListEventsclient.Events.Listclient.events.listclient.events.list
RetrieveEventclient.Events.Retrieveclient.events.retrieveclient.events.retrieve
ResendEventclient.Events.Resendclient.events.resendclient.events.resend

The SDK helper for verifying inbound deliveries (ConstructEvent / construct_event / constructEvent) is documented under Signature verification.


Resources

The WebhookEndpoint object

AttributeTypeDescription
idstringUnique identifier. Prefixed with whe_.
app_idstringParent app ID.
urlstringHTTPS delivery URL.
descriptionstringOptional, ≤ 500 chars. Omitted if unset.
enabled_eventsstring[]Subscribed event types, or ["*"] for all.
statusenumenabled or disabled.
disabled_reasonstringmanual (you paused it) or auto_failures (auto-disabled). Present only when status is disabled.
api_versionstringAPI version pinned for this endpoint’s deliveries.
metadatamapYour string key/value pairs.
secretstringSigning secret (whsec_…). Returned only on Create and Rotate — never on retrieve/list.
last_rotated_atstringRFC 3339. Present after the first rotation.
previous_secret_expires_atstringRFC 3339. Present during the 24-hour rotation overlap, when the previous secret still signs deliveries.
created_atstringRFC 3339 timestamp.
updated_atstringRFC 3339 timestamp.

The Event object

AttributeTypeDescription
idstringUnique identifier. Prefixed with evt_. Matches the Webhook-Id delivery header.
objectstringAlways "event".
app_idstringParent app ID.
typestringEvent type, e.g. "job.succeeded". See the event catalog.
datastringJSON-encoded resource snapshot. Parse it to get the resource (which carries its own object discriminator). On the delivery wire this same snapshot is a nested object — see Wire format.
request_idstringID of the API request that triggered the event. May be empty for system-generated events.
pending_webhooksintDelivery attempts still pending across all subscribed endpoints.
api_versionstringAPI version frozen at emit time.
livemodebooleanReserved. Always true today.
created_atstringRFC 3339 timestamp.

The WebhookDelivery object

AttributeTypeDescription
idstringUnique identifier. Prefixed with whd_.
webhook_endpoint_idstringEndpoint this attempt targeted.
event_idstringEvent delivered.
statusenumpending, succeeded, or failed.
attemptint1-based attempt number (1–15).
response_statusintHTTP status your endpoint returned. Absent on a transport error.
response_bodystringFirst 4 KiB of your response body. Absent if none.
response_headersstringJSON-encoded response headers (≤ 32 names / 4 KiB; credential-bearing headers redacted to [REDACTED]). Absent if none.
latency_msintRound-trip time in milliseconds. Absent if no response was received.
transport_errorstringPresent on a transport failure: one of timeout, connection_refused, dns_failure, tls_handshake_failed, ssrf_blocked, unknown.
next_attempt_atstringWhen the next retry is scheduled. Absent on succeeded or terminally-failed deliveries.
created_atstringRFC 3339 timestamp.
updated_atstringRFC 3339 timestamp.

Endpoints

Create an endpoint

POST /transcodely.v1.WebhookService/CreateWebhookEndpoint

curl -X POST https://api.transcodely.com/transcodely.v1.WebhookService/CreateWebhookEndpoint 
  -H "Authorization: Bearer ak_live_..." 
  -H "Content-Type: application/json" 
  -d '{
    "app_id": "app_default000",
    "url": "https://example.com/webhooks/transcodely",
    "description": "Production webhook endpoint",
    "enabled_events": ["job.succeeded", "job.failed"],
    "metadata": { "env": "prod" }
  }'
endpoint, err := client.WebhookEndpoints.Create(ctx, &transcodely.WebhookEndpointCreateParams{
	AppId:         "app_default000",
	Url:           "https://example.com/webhooks/transcodely",
	EnabledEvents: []string{"job.succeeded", "job.failed"},
	Metadata:      map[string]string{"env": "prod"},
})
// endpoint.GetSecret() carries the whsec_… secret — returned only here.
endpoint = client.webhook_endpoints.create(
    app_id="app_default000",
    url="https://example.com/webhooks/transcodely",
    enabled_events=["job.succeeded", "job.failed"],
    description="Production webhook endpoint",
    metadata={"env": "prod"},
)
# endpoint.secret carries the whsec_… secret — returned only here.
const endpoint = await client.webhookEndpoints.create({
  appId: "app_default000",
  url: "https://example.com/webhooks/transcodely",
  enabledEvents: ["job.succeeded", "job.failed"],
  description: "Production webhook endpoint",
  metadata: { env: "prod" },
});
// endpoint.secret carries the whsec_… secret — returned only here.

Response — the only call that returns the plaintext secret:

{
  "endpoint": {
    "id": "whe_a1b2c3d4e5f6",
    "app_id": "app_default000",
    "url": "https://example.com/webhooks/transcodely",
    "description": "Production webhook endpoint",
    "enabled_events": ["job.succeeded", "job.failed"],
    "status": "enabled",
    "api_version": "2026-05-23",
    "metadata": { "env": "prod" },
    "created_at": "2026-05-24T10:42:11Z",
    "updated_at": "2026-05-24T10:42:11Z",
    "secret": "whsec_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0"
  }
}

Validation:

  • url must start with https:// and resolve to a public IP. Private/loopback/metadata IPs are rejected.
  • enabled_events requires ≥ 1 item. Use "*" for all event types (now and in the future).
  • description ≤ 500 characters.

Retrieve an endpoint

POST /transcodely.v1.WebhookService/RetrieveWebhookEndpoint with { "id": "whe_..." }. Returns the same shape without the secret field.

Update an endpoint

POST /transcodely.v1.WebhookService/UpdateWebhookEndpoint. Only fields you set are applied; omitting a field leaves it unchanged.

curl -X POST https://api.transcodely.com/transcodely.v1.WebhookService/UpdateWebhookEndpoint 
  -H "Authorization: Bearer ak_live_..." 
  -d '{
    "id": "whe_a1b2c3d4e5f6",
    "enabled_events": ["job.succeeded", "job.failed", "output.ready"]
  }'
endpoint, err := client.WebhookEndpoints.Update(ctx, &transcodely.WebhookEndpointUpdateParams{
	Id:            "whe_a1b2c3d4e5f6",
	EnabledEvents: []string{"job.succeeded", "job.failed", "output.ready"},
})
endpoint = client.webhook_endpoints.update(
    "whe_a1b2c3d4e5f6",
    enabled_events=["job.succeeded", "job.failed", "output.ready"],
)
const endpoint = await client.webhookEndpoints.update({
  id: "whe_a1b2c3d4e5f6",
  enabledEvents: ["job.succeeded", "job.failed", "output.ready"],
});

Pause an endpoint by setting "status": "disabled" (re-enable with "enabled"). Caveats — the server treats zero-values as “no change”:

  • description: "" leaves the existing description in place (cannot be cleared).
  • enabled_events: [] is not allowed — pass at least the events you want to keep (the list is replaced wholesale).
  • metadata: {} leaves the existing map in place.

Delete an endpoint

POST /transcodely.v1.WebhookService/DeleteWebhookEndpoint with { "id": "whe_..." } (soft delete; delivery history is preserved).

List endpoints

POST /transcodely.v1.WebhookService/ListWebhookEndpoints with cursor pagination (limit ≤ 100, default 20).

curl -X POST https://api.transcodely.com/transcodely.v1.WebhookService/ListWebhookEndpoints 
  -H "Authorization: Bearer ak_live_..." 
  -d '{ "app_id": "app_default000", "pagination": { "limit": 20 } }'
iter := client.WebhookEndpoints.List(ctx, &transcodely.WebhookEndpointListParams{AppId: "app_default000"})
for iter.Next() {
	endpoint := iter.Current()
	_ = endpoint
}
if err := iter.Err(); err != nil {
	log.Fatal(err)
}
for endpoint in client.webhook_endpoints.list(app_id="app_default000").auto_paging_iter():
    print(endpoint.id, endpoint.url)
for await (const endpoint of client.webhookEndpoints.list({ appId: "app_default000" }).autoPage()) {
  console.log(endpoint.id, endpoint.url);
}
{
  "endpoints": [/* WebhookEndpoint[], no secrets */],
  "pagination": { "next_cursor": "…" }
}

Rotate the signing secret

POST /transcodely.v1.WebhookService/RotateWebhookSecret with { "id": "whe_..." }. SDK: RotateSecret / rotate_secret / rotateSecret.

The new plaintext secret is returned in the response. The previous secret remains valid for signing for 24 hours so in-flight requests don’t break — during that window Transcodely sends two v1= entries in the Transcodely-Signature header, and previous_secret_expires_at marks the end of the overlap. Verify against both secrets through the window (see Rotating the signing secret).

curl -X POST https://api.transcodely.com/transcodely.v1.WebhookService/RotateWebhookSecret 
  -H "Authorization: Bearer ak_live_..." 
  -d '{ "id": "whe_a1b2c3d4e5f6" }'
endpoint, err := client.WebhookEndpoints.RotateSecret(ctx, "whe_a1b2c3d4e5f6")
// endpoint.GetSecret() carries the new whsec_… secret.
endpoint = client.webhook_endpoints.rotate_secret("whe_a1b2c3d4e5f6")
# endpoint.secret carries the new whsec_… secret.
const endpoint = await client.webhookEndpoints.rotateSecret("whe_a1b2c3d4e5f6");
// endpoint.secret carries the new whsec_… secret.
{
  "endpoint": {
    "id": "whe_a1b2c3d4e5f6",
    "status": "enabled",
    "secret": "whsec_<new-secret>",
    "last_rotated_at": "2026-05-24T12:00:00Z",
    "previous_secret_expires_at": "2026-05-25T12:00:00Z",
    "...": "..."
  }
}

Send a test event

POST /transcodely.v1.WebhookService/SendTestWebhook. Delivers a synthetic event of the given type to a single endpoint through the normal signed pipeline, and returns the resulting WebhookDelivery to inspect.

curl -X POST https://api.transcodely.com/transcodely.v1.WebhookService/SendTestWebhook 
  -H "Authorization: Bearer ak_live_..." 
  -d '{ "endpoint_id": "whe_a1b2c3d4e5f6", "event_type": "job.succeeded" }'
delivery, err := client.WebhookEndpoints.SendTest(ctx, "whe_a1b2c3d4e5f6", transcodely.EventTypeJobSucceeded)
delivery = client.webhook_endpoints.send_test("whe_a1b2c3d4e5f6", "job.succeeded")
const delivery = await client.webhookEndpoints.sendTest("whe_a1b2c3d4e5f6", "job.succeeded");
{
  "delivery": {
    "id": "whd_n5EbdoJlHZ0Y6j",
    "webhook_endpoint_id": "whe_a1b2c3d4e5f6",
    "event_id": "evt_test_7x6w5v4u3t2s1r0q",
    "status": "pending",
    "attempt": 1,
    "created_at": "2026-05-24T12:05:00Z",
    "updated_at": "2026-05-24T12:05:00Z"
  }
}
  • event_type must be a concrete type — the "*" wildcard is rejected.
  • Test events use an evt_test_ ID prefix, are invisible to ListEvents, and never bump pending_webhooks on real events.
  • Rate-limited to 10 calls per minute per endpoint. Disabled endpoints reject the call with failed_precondition.

Events

List events

POST /transcodely.v1.WebhookService/ListEvents. SDK: client.Events.List / client.events.list.

curl -X POST https://api.transcodely.com/transcodely.v1.WebhookService/ListEvents 
  -H "Authorization: Bearer ak_live_..." 
  -d '{
    "app_id": "app_default000",
    "type": "job.succeeded",
    "created_after":  "2026-05-20T00:00:00Z",
    "created_before": "2026-05-24T23:59:59Z",
    "pagination": { "limit": 50 }
  }'
iter := client.Events.List(ctx, &transcodely.EventListParams{
	AppId:         "app_default000",
	Type:          proto.String("job.succeeded"),
	CreatedAfter:  timestamppb.New(time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC)),
	CreatedBefore: timestamppb.New(time.Date(2026, 5, 24, 23, 59, 59, 0, time.UTC)),
	Pagination:    &transcodely.PaginationRequest{Limit: 50},
})
for iter.Next() {
	event := iter.Current()
	_ = event
}
if err := iter.Err(); err != nil {
	log.Fatal(err)
}
for event in client.events.list(
    app_id="app_default000",
    type="job.succeeded",
    created_after=datetime(2026, 5, 20, tzinfo=timezone.utc),
    created_before=datetime(2026, 5, 24, 23, 59, 59, tzinfo=timezone.utc),
    limit=50,
).auto_paging_iter():
    print(event.id, event.type)
const events = client.events.list({
  appId: "app_default000",
  type: "job.succeeded",
  createdAfter: Timestamp.fromDate(new Date("2026-05-20T00:00:00Z")),
  createdBefore: Timestamp.fromDate(new Date("2026-05-24T23:59:59Z")),
  pagination: { limit: 50 },
});
for await (const event of events.autoPage()) {
  console.log(event.id, event.type);
}

Response — newest first:

{
  "events": [
    {
      "id": "evt_a1b2c3d4e5f6g7h8",
      "object": "event",
      "app_id": "app_default000",
      "type": "job.succeeded",
      "data": "{"id":"job_…","object":"job","status":"completed",…}",
      "request_id": "req_z9y8x7w6v5u4t3s2",
      "pending_webhooks": 0,
      "api_version": "2026-05-23",
      "livemode": true,
      "created_at": "2026-05-24T10:55:08Z"
    }
  ],
  "pagination": { "next_cursor": "…" }
}

type, created_after, and created_before are optional filters. event.data is a JSON-encoded string (not a nested object) — parse it with JSON.parse(event.data) to get the resource snapshot, which is exactly what your endpoint receives inside the envelope’s data field on the wire.

Retrieve a single event

POST /transcodely.v1.WebhookService/RetrieveEvent with { "id": "evt_..." }.

Resend an event

POST /transcodely.v1.WebhookService/ResendEvent. SDK: client.Events.Resend / client.events.resend.

# Resend to all currently-subscribed enabled endpoints
curl -X POST https://api.transcodely.com/transcodely.v1.WebhookService/ResendEvent 
  -H "Authorization: Bearer ak_live_..." 
  -d '{ "id": "evt_a1b2c3d4e5f6g7h8", "endpoint_ids": [] }'

# Resend to a specific endpoint subset (max 100)
curl -X POST https://api.transcodely.com/transcodely.v1.WebhookService/ResendEvent 
  -H "Authorization: Bearer ak_live_..." 
  -d '{
    "id": "evt_a1b2c3d4e5f6g7h8",
    "endpoint_ids": ["whe_a1b2c3d4e5f6"]
  }'
// Resend to all currently-subscribed enabled endpoints.
deliveries, err := client.Events.Resend(ctx, "evt_a1b2c3d4e5f6g7h8")

// Resend to a specific endpoint subset (variadic; max 100).
deliveries, err = client.Events.Resend(ctx, "evt_a1b2c3d4e5f6g7h8", "whe_a1b2c3d4e5f6")
# Resend to all currently-subscribed enabled endpoints.
deliveries = client.events.resend("evt_a1b2c3d4e5f6g7h8")

# Resend to a specific endpoint subset (max 100).
deliveries = client.events.resend(
    "evt_a1b2c3d4e5f6g7h8",
    endpoint_ids=["whe_a1b2c3d4e5f6"],
)
// Resend to all currently-subscribed enabled endpoints.
const deliveries = await client.events.resend("evt_a1b2c3d4e5f6g7h8");

// Resend to a specific endpoint subset (max 100).
const subset = await client.events.resend("evt_a1b2c3d4e5f6g7h8", {
  endpointIds: ["whe_a1b2c3d4e5f6"],
});

A resend reuses the original event ID, so a correct receiver deduplicates it. Response: one WebhookDelivery row per endpoint the event was queued to (status pending, attempt: 1).


Deliveries

List delivery attempts

POST /transcodely.v1.WebhookService/ListWebhookDeliveries. Pass endpoint_id, event_id, or both — at least one is required. SDK: ListDeliveries / list_deliveries / listDeliveries.

curl -X POST https://api.transcodely.com/transcodely.v1.WebhookService/ListWebhookDeliveries 
  -H "Authorization: Bearer ak_live_..." 
  -d '{
    "endpoint_id": "whe_a1b2c3d4e5f6",
    "status": "failed",
    "pagination": { "limit": 50 }
  }'
iter := client.WebhookEndpoints.ListDeliveries(ctx, &transcodely.WebhookDeliveryListParams{
	EndpointId: proto.String("whe_a1b2c3d4e5f6"),
	Status:     proto.String("failed"),
	Pagination: &transcodely.PaginationRequest{Limit: 50},
})
for iter.Next() {
	delivery := iter.Current()
	_ = delivery
}
if err := iter.Err(); err != nil {
	log.Fatal(err)
}
for delivery in client.webhook_endpoints.list_deliveries(
    endpoint_id="whe_a1b2c3d4e5f6",
    status="failed",
    limit=50,
).auto_paging_iter():
    print(delivery.id, delivery.status)
const deliveries = client.webhookEndpoints.listDeliveries({
  endpointId: "whe_a1b2c3d4e5f6",
  status: "failed",
  pagination: { limit: 50 },
});
for await (const delivery of deliveries.autoPage()) {
  console.log(delivery.id, delivery.status);
}

Response — newest first:

{
  "deliveries": [
    {
      "id": "whd_n5EbdoJlHZ0Y6j",
      "webhook_endpoint_id": "whe_a1b2c3d4e5f6",
      "event_id": "evt_a1b2c3d4e5f6g7h8",
      "status": "failed",
      "attempt": 3,
      "response_status": 503,
      "response_body": "{"error":"upstream unavailable"}",
      "response_headers": "{"content-type":"application/json"}",
      "latency_ms": 214,
      "next_attempt_at": "2026-05-24T11:25:08Z",
      "created_at": "2026-05-24T10:55:08Z",
      "updated_at": "2026-05-24T10:55:10Z"
    },
    {
      "id": "whd_m4DacnIkGY9X5i",
      "webhook_endpoint_id": "whe_a1b2c3d4e5f6",
      "event_id": "evt_b2c3d4e5f6g7h8i9",
      "status": "failed",
      "attempt": 1,
      "transport_error": "timeout",
      "next_attempt_at": "2026-05-24T10:56:08Z",
      "created_at": "2026-05-24T10:55:08Z",
      "updated_at": "2026-05-24T10:55:18Z"
    }
  ],
  "pagination": { "next_cursor": "…" }
}
  • response_status, response_body, response_headers, and latency_ms are present only when your endpoint actually responded.
  • transport_error is present instead when the request never completed (timeout, DNS failure, refused connection, TLS handshake failure, SSRF block).
  • next_attempt_at is absent on succeeded and terminally-failed deliveries.
  • Filter by status (pending / succeeded / failed).

Endpoint health

POST /transcodely.v1.WebhookService/GetEndpointHealth. Aggregate delivery stats for one endpoint over a rolling window"24h" (24 hourly buckets, default), "7d" (7 daily buckets), or "30d" (30 daily buckets). SDK: GetHealth / get_health / getHealth. The response is cached server-side for ~30 s.

curl -X POST https://api.transcodely.com/transcodely.v1.WebhookService/GetEndpointHealth 
  -H "Authorization: Bearer ak_live_..." 
  -d '{ "endpoint_id": "whe_a1b2c3d4e5f6", "window": "24h" }'
health, err := client.WebhookEndpoints.GetHealth(ctx, "whe_a1b2c3d4e5f6", transcodely.HealthWindow24h)
// health.GetSuccessRate(), health.GetP95LatencyMs(), health.GetBuckets()
health = client.webhook_endpoints.get_health("whe_a1b2c3d4e5f6", "24h")
# health.success_rate, health.p95_latency_ms, health.buckets
const health = await client.webhookEndpoints.getHealth("whe_a1b2c3d4e5f6", "24h");
// health.successRate, health.p95LatencyMs, health.buckets
{
  "window": "24h",
  "total_attempts": 1280,
  "succeeded": 1270,
  "failed": 10,
  "pending": 0,
  "success_rate": 0.9921875,
  "p50_latency_ms": 88,
  "p95_latency_ms": 240,
  "buckets": [
    { "bucket_start": "2026-05-23T13:00:00Z", "attempts": 52, "succeeded": 52, "failed": 0, "pending": 0 },
    { "bucket_start": "2026-05-23T14:00:00Z", "attempts": 60, "succeeded": 58, "failed": 2, "pending": 0 }
  ]
}

success_rate is succeeded / (succeeded + failed). p50_latency_ms / p95_latency_ms are omitted when no responses were received in the window. buckets is zero-filled to the full window length so the array length is stable (24, 7, or 30).


Wire format

The HTTP request your endpoint actually receives:

POST https://example.com/webhooks/transcodely HTTP/1.1
Content-Type: application/json
Webhook-Id: evt_a1b2c3d4e5f6g7h8
Transcodely-Signature: t=1716480293,v1=a3f7b2…

{
  "id": "evt_a1b2c3d4e5f6g7h8",
  "object": "event",
  "api_version": "2026-05-23",
  "created": "2026-05-24T10:55:08Z",
  "type": "job.succeeded",
  "data": { "id": "job_…", "object": "job", "status": "completed", … },
  "livemode": true,
  "pending_webhooks": 0,
  "request": { "id": "req_…", "idempotency_key": null }
}
  • Webhook-Id — the event ID. Use this for idempotency — retries carry the same ID.
  • Transcodely-Signature — see Signature verification.
  • The body is a flat envelope. data is the resource snapshot, byte-for-byte identical to what Get<Resource> returns. The inner data.object ("job" / "job_output" / "video" / "app") is a stable discriminator.
  • HMAC is computed over the raw envelope bytes — exactly the bytes your server reads from the request body.

Your endpoint must reply with a 2xx status within 10 seconds. Anything else (including a timeout) is treated as a failure and triggers the retry curve.


Errors

Connect codeerror_code slugCause
unauthenticatedAPI key missing/invalid or not app-scoped.
not_foundwebhook_endpoint_not_foundwhe_… doesn’t exist or belongs to another app.
not_foundwebhook_event_not_foundevt_… doesn’t exist or belongs to another app.
invalid_argumentwebhook_url_invalidURL is private/loopback or otherwise SSRF-blocked.
invalid_argument(protovalidate)URL not HTTPS, empty enabled_events, "*" passed to SendTestWebhook, etc.
failed_preconditionSendTestWebhook on a disabled endpoint.
resource_exhaustedSendTestWebhook rate limit (10/min per endpoint) exceeded.
internalUnexpected server error — retry idempotent calls.

The error_code slug is delivered as a header on the error metadata.