media.delivery.completed
Composio upload of listing media to the agent's cloud storage finished successfully. summary.files_failed may be > 0 in the partial-success case (some files uploaded, some files skipped) - this is still considered a completion.
Category: Media
Emitted when: deliver_media_to_storage Celery task returns success (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.completed 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 media bundle was uploaded to. folder_path is the human-visible path within the storage provider's root (e.g. "/Listings/HMREK1T"). Must be pre- sanitized per CLAUDE.md rule 30 before being placed on the payload; the validator below is a defence-in-depth check that would surface a buggy emission site in tests rather than leak malformed paths to integrators. |
summary | object | yes | Aggregate counts for the delivery job. Soft constraint (NOT enforced on the model because we do not carry the original batch size into the payload): files_uploaded + files_failed <= original_batch_size. That constraint is enforced at the call site, which has access to the batch size before emission. Using StrictInt rejects bool values (True would otherwise coerce to 1 and masquerade as a real count). Negative counts are rejected by ge=0. |
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_6CS9YQE76ND4JPG6BD1RDAGXAT",
"event_type": "media.delivery.completed",
"api_version": "2026-04-17",
"timestamp": 1745339401,
"nonce": "285BX2EMYT9AD69QT3J4RSK7FS",
"data": {
"agency_id": "user_01HXAGENCY0000000000000",
"listing_number": "abc1234",
"job_id": "01HX5Y7Z2M3N4P5Q6R7S8T9U0V",
"destination": {
"kind": "google_drive",
"folder_path": "/Listings/123-Main-St-Portland/Photos"
},
"summary": {
"files_uploaded": 0,
"files_failed": 0,
"total_bytes": 0
},
"duration_seconds": 0
}
}
Signature header (delivered as X-Webhook-Signature on the same request):
sha256=f94c522ee2969ecb0d2787a47caf7a1bcd3491ec574726bf76e87ed2e6fe18f5
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.