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.
| RPC | Go | Python | JavaScript |
|---|---|---|---|
CreateWebhookEndpoint | client.WebhookEndpoints.Create | client.webhook_endpoints.create | client.webhookEndpoints.create |
RetrieveWebhookEndpoint | client.WebhookEndpoints.Retrieve | client.webhook_endpoints.retrieve | client.webhookEndpoints.retrieve |
UpdateWebhookEndpoint | client.WebhookEndpoints.Update | client.webhook_endpoints.update | client.webhookEndpoints.update |
DeleteWebhookEndpoint | client.WebhookEndpoints.Delete | client.webhook_endpoints.delete | client.webhookEndpoints.delete |
ListWebhookEndpoints | client.WebhookEndpoints.List | client.webhook_endpoints.list | client.webhookEndpoints.list |
RotateWebhookSecret | client.WebhookEndpoints.RotateSecret | client.webhook_endpoints.rotate_secret | client.webhookEndpoints.rotateSecret |
SendTestWebhook | client.WebhookEndpoints.SendTest | client.webhook_endpoints.send_test | client.webhookEndpoints.sendTest |
ListWebhookDeliveries | client.WebhookEndpoints.ListDeliveries | client.webhook_endpoints.list_deliveries | client.webhookEndpoints.listDeliveries |
GetEndpointHealth | client.WebhookEndpoints.GetHealth | client.webhook_endpoints.get_health | client.webhookEndpoints.getHealth |
ListEvents | client.Events.List | client.events.list | client.events.list |
RetrieveEvent | client.Events.Retrieve | client.events.retrieve | client.events.retrieve |
ResendEvent | client.Events.Resend | client.events.resend | client.events.resend |
The SDK helper for verifying inbound deliveries (ConstructEvent / construct_event / constructEvent) is documented under Signature verification.
Resources
The WebhookEndpoint object
| Attribute | Type | Description |
|---|---|---|
id | string | Unique identifier. Prefixed with whe_. |
app_id | string | Parent app ID. |
url | string | HTTPS delivery URL. |
description | string | Optional, ≤ 500 chars. Omitted if unset. |
enabled_events | string[] | Subscribed event types, or ["*"] for all. |
status | enum | enabled or disabled. |
disabled_reason | string | manual (you paused it) or auto_failures (auto-disabled). Present only when status is disabled. |
api_version | string | API version pinned for this endpoint’s deliveries. |
metadata | map | Your string key/value pairs. |
secret | string | Signing secret (whsec_…). Returned only on Create and Rotate — never on retrieve/list. |
last_rotated_at | string | RFC 3339. Present after the first rotation. |
previous_secret_expires_at | string | RFC 3339. Present during the 24-hour rotation overlap, when the previous secret still signs deliveries. |
created_at | string | RFC 3339 timestamp. |
updated_at | string | RFC 3339 timestamp. |
The Event object
| Attribute | Type | Description |
|---|---|---|
id | string | Unique identifier. Prefixed with evt_. Matches the Webhook-Id delivery header. |
object | string | Always "event". |
app_id | string | Parent app ID. |
type | string | Event type, e.g. "job.succeeded". See the event catalog. |
data | string | JSON-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_id | string | ID of the API request that triggered the event. May be empty for system-generated events. |
pending_webhooks | int | Delivery attempts still pending across all subscribed endpoints. |
api_version | string | API version frozen at emit time. |
livemode | boolean | Reserved. Always true today. |
created_at | string | RFC 3339 timestamp. |
The WebhookDelivery object
| Attribute | Type | Description |
|---|---|---|
id | string | Unique identifier. Prefixed with whd_. |
webhook_endpoint_id | string | Endpoint this attempt targeted. |
event_id | string | Event delivered. |
status | enum | pending, succeeded, or failed. |
attempt | int | 1-based attempt number (1–15). |
response_status | int | HTTP status your endpoint returned. Absent on a transport error. |
response_body | string | First 4 KiB of your response body. Absent if none. |
response_headers | string | JSON-encoded response headers (≤ 32 names / 4 KiB; credential-bearing headers redacted to [REDACTED]). Absent if none. |
latency_ms | int | Round-trip time in milliseconds. Absent if no response was received. |
transport_error | string | Present on a transport failure: one of timeout, connection_refused, dns_failure, tls_handshake_failed, ssrf_blocked, unknown. |
next_attempt_at | string | When the next retry is scheduled. Absent on succeeded or terminally-failed deliveries. |
created_at | string | RFC 3339 timestamp. |
updated_at | string | RFC 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:
urlmust start withhttps://and resolve to a public IP. Private/loopback/metadata IPs are rejected.enabled_eventsrequires ≥ 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_typemust be a concrete type — the"*"wildcard is rejected.- Test events use an
evt_test_ID prefix, are invisible toListEvents, and never bumppending_webhookson 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, andlatency_msare present only when your endpoint actually responded.transport_erroris present instead when the request never completed (timeout, DNS failure, refused connection, TLS handshake failure, SSRF block).next_attempt_atis 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.bucketsconst 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.
datais the resource snapshot, byte-for-byte identical to whatGet<Resource>returns. The innerdata.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 code | error_code slug | Cause |
|---|---|---|
unauthenticated | — | API key missing/invalid or not app-scoped. |
not_found | webhook_endpoint_not_found | whe_… doesn’t exist or belongs to another app. |
not_found | webhook_event_not_found | evt_… doesn’t exist or belongs to another app. |
invalid_argument | webhook_url_invalid | URL is private/loopback or otherwise SSRF-blocked. |
invalid_argument | (protovalidate) | URL not HTTPS, empty enabled_events, "*" passed to SendTestWebhook, etc. |
failed_precondition | — | SendTestWebhook on a disabled endpoint. |
resource_exhausted | — | SendTestWebhook rate limit (10/min per endpoint) exceeded. |
internal | — | Unexpected server error — retry idempotent calls. |
The error_code slug is delivered as a header on the error metadata.