media.delivery.failed
Composio upload of listing media to the agent's cloud storage failed after exhausting all retries. NO files were uploaded (for partial-success, subscribe to media.delivery.completed instead - the summary there carries files_failed counts).
Category: Media
Emitted when: deliver_media_to_storage Celery task raised its terminal exception after retries exhausted (app/tasks/composio_tasks.py).
Envelope
Every delivery wraps the event-specific data block in the canonical
envelope documented on the
webhook overview page. The body carries SIX keys;
the HMAC signature is delivered as the X-Webhook-Signature request
header on the inbound POST to your endpoint (NOT as a body field;
including it in the body would make the HMAC a function of its own
output).
| Field | Type | Description |
|---|---|---|
event_id | string | ULID; globally unique. Safe to persist as the dedup key. |
event_type | string | Always media.delivery.failed for this event. |
api_version | string | Payload schema version (YYYY-MM-DD). |
timestamp | integer | Unix epoch seconds at dispatch time. |
nonce | string | ULID; unique per delivery, used for replay rejection. |
data | object | Event-specific payload (see below). |
Delivery also sets three HTTP headers on the inbound POST:
| Header | Meaning |
|---|---|
X-Webhook-Event-Id | Mirrors event_id; cheaper than parsing the body to dedup. |
X-Webhook-Timestamp | Mirrors timestamp for replay-window checks. |
X-Webhook-Signature | sha256=<hex> HMAC over timestamp.body. |
Data schema
| Field | Type | Required | Description |
|---|---|---|---|
agency_id | string | yes | Agency Id |
listing_number | string | yes | Public listing number, 7-char lowercase nanoid ([a-z0-9]{7}) |
job_id | string | yes | 'deliver_ |
destination | object | yes | Where the upload was TARGETED (even though it failed). Mirrors the DeliveryDestination in media_delivery_completed by design: subscribers that parse both events should be able to use the same destination deserializer for either variant. The classes live in separate modules to keep each event file independently importable (the app/events/data_models/__init__.py eager-import pattern does not assume cross-module imports between event files). |
error | object | yes | Normalized error block. code is always a stable short string from the caller-side whitelist (e.g. "composio_401", "composio_rate_limited", "composio_5xx", "composio_unknown"). Integrators branch on the code, not the message - which is locale / version specific and subject to drift. message is a truncated (<= 1024 chars) human-readable description of the underlying failure. Truncation prevents a pathological exception message from bloating the webhook envelope beyond what the dispatcher's size limits accept. |
duration_seconds | integer | yes | Duration Seconds |
Sample payload
The sample below is a live fixture. Feed the exact body bytes shown
plus the timestamp into the HMAC-SHA256 recipe from the
webhook overview using the
documented test-vector secret (test_secret_001 )
and you will recover the signature header value printed below.
Body (delivered as the HTTP request body):
{
"event_id": "evt_1MZQKGSKEZ7F23VNSPJ066DR0C",
"event_type": "media.delivery.failed",
"api_version": "2026-04-17",
"timestamp": 1745339401,
"nonce": "63M1E1KPHN1KWX0N6AEJZW364M",
"data": {
"agency_id": "user_01HXAGENCY0000000000000",
"listing_number": "abc1234",
"job_id": "01HX5Y7Z2M3N4P5Q6R7S8T9U0V",
"destination": {
"kind": "google_drive",
"folder_path": "/Listings/123-Main-St-Portland/Photos"
},
"error": {
"code": "media.delivery.failed.bucket_403",
"message": "Destination bucket rejected upload: 403 Forbidden on PUT."
},
"duration_seconds": 0
}
}
Signature header (delivered as X-Webhook-Signature on the same request):
sha256=c8b602b2ec1f4cded09036aab88c212ea9a07efa5c5163c71ba180728199a8a6
Retry behaviour
Failed deliveries retry on the sequence 2, 4, 8, 16, 32 seconds
(five attempts plus the initial call for a total of six over
approximately 62 seconds of wall clock). After the final attempt the
delivery lands in the dead-letter queue and the dashboard operator can
replay it from Settings > API Keys > Webhook Endpoint > Deliveries.
See the webhook overview for the full
rules.
Verification
Every receiver MUST verify the X-Webhook-Signature header using the
recipe in the webhook overview.
Rejecting deliveries whose timestamp is more than 5 minutes off your
wall clock (after NTP correction on your side) or whose nonce has
already been consumed in the last 10 minutes is part of the contract.