Get Result
Fetch the stored result of a completed request — no webhook required.
GET /cargo/{cargo_id}/resultConvoy stores every result server-side for 30 days (configurable per request
with result_ttl_seconds on /cargo/load). This endpoint
returns that stored result in the exact same shape as the callback webhook
payload, so your parsing code works identically whether the result arrived
by webhook or by polling.
Use it to:
- Skip webhooks entirely — submit with no
callback_url(“mailbox-only”) and poll for the result when you’re ready. - Recover from
callback_failed— if Convoy exhausted its webhook retries, the result is still here. - Fetch large results on demand — agents, scripts, and CLIs that have no public HTTPS endpoint.
Authentication
This endpoint requires the X-API-Key header with a valid project API key.
-H "X-API-Key: convoy_sk_your_key_here"Project Scoping: You can only fetch results for cargo that belongs to
your project. Cargo from another project returns 404 Not Found.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
cargo_id | string | The cargo ID from /cargo/load response |
Response
Same shape as the webhook callback payload:
{
"cargo_id": "cargo_abc123def456",
"success": true,
"response": {
"id": "msg_01ABC...",
"content": [{"type": "text", "text": "..."}],
"usage": {"input_tokens": 25, "output_tokens": 150}
},
"error_message": null,
"created_at": "2024-01-15T11:45:00Z",
"metadata": {"job": "nightly-42"}
}| Field | Description |
|---|---|
cargo_id | Request identifier |
success | Whether processing succeeded |
response | Full model response (null on failure) |
error_message | Error details when success is false |
created_at | When the result was produced |
metadata | Your opaque metadata from /cargo/load, echoed verbatim (null if none) |
Status Codes
| Status | Meaning |
|---|---|
200 | Result available — body above (also for failed cargo, with success: false) |
401 | Missing or invalid API key |
403 | Project is inactive |
404 | Cargo not found or not owned by your project |
409 | Cargo is not finished yet — keep polling (current status in body) |
410 | Result expired and was purged (past its retention TTL) |
429 | Rate limit exceeded |
409 — not finished yet
{
"detail": {
"error": "result_not_ready",
"message": "Cargo cargo_abc123def456 is not finished yet (status: processing). ...",
"cargo_id": "cargo_abc123def456",
"status": "processing",
"status_description": "Batch is being processed by the provider"
}
}410 — expired
{
"detail": {
"error": "result_expired",
"message": "The result for cargo cargo_abc123def456 has expired and is no longer retrievable.",
"cargo_id": "cargo_abc123def456",
"status": "completed"
}
}Results are retained for 30 days by default (or the
result_ttl_seconds you set at submission — 1 hour to 30 days). After
that they are permanently purged and this endpoint returns 410 Gone.
Fetch and persist results you need long-term.
Mailbox-only submission (no webhook)
callback_url is optional on /cargo/load. When omitted, no webhook is
ever sent — the cargo terminates at completed or failed and you retrieve
the result here:
import httpx
import time
API = "https://api.cnvy.ai"
HEADERS = {"X-API-Key": "convoy_sk_your_key_here"}
# 1. Submit WITHOUT a callback_url
submit = httpx.post(f"{API}/cargo/load", headers=HEADERS, json={
"params": {
"model": "claude-haiku-4-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Summarize this document..."}],
},
# no callback_url — mailbox-only
"result_ttl_seconds": 604800, # keep the result for 7 days (optional)
})
cargo_id = submit.json()["cargo_id"]
# 2. Poll for the result (409 = not done yet)
while True:
r = httpx.get(f"{API}/cargo/{cargo_id}/result", headers=HEADERS)
if r.status_code == 200:
result = r.json()
break
if r.status_code == 409:
time.sleep(30)
continue
r.raise_for_status()
if result["success"]:
print(result["response"]["content"][0]["text"])
else:
print(f"Failed: {result['error_message']}")Polling etiquette: batch results take minutes to hours. Poll
/cargo/{cargo_id}/tracking (cheap, status-only) at your chosen interval
and fetch /result once the status is terminal — or just poll /result
directly and treat 409 as “not yet”. Either works; tracking bodies are
smaller.
Recovering from callback_failed
If your webhook endpoint was down and Convoy exhausted its retry schedule,
the cargo lands in callback_failed — but the result is not lost:
curl -H "X-API-Key: convoy_sk_your_key_here" \
https://api.cnvy.ai/cargo/cargo_abc123def456/resultReturns the full result payload exactly as the webhook would have delivered it.
Examples
curl
curl -H "X-API-Key: convoy_sk_your_key_here" \
https://api.cnvy.ai/cargo/cargo_abc123def456/resultJavaScript
async function getResult(cargoId, apiKey) {
const response = await fetch(
`https://api.cnvy.ai/cargo/${cargoId}/result`,
{ headers: { "X-API-Key": apiKey } }
);
if (response.status === 409) return null; // not finished yet
if (response.status === 410) throw new Error("Result expired");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}When to use webhooks vs the mailbox
Webhook (callback_url) | Mailbox (/result) | |
|---|---|---|
| Latency | Push — arrives seconds after completion | Poll — bounded by your interval |
| Infrastructure | Needs a public HTTPS endpoint | None |
| Reliability | 5 attempts over ~40 minutes, then callback_failed | Result stored up to 30 days |
| Best for | Servers, pipelines, AWS Lambda durable functions | Agents, scripts, CLIs, notebooks |
You can use both: provide a callback_url for push latency and fall back
to /result if the webhook is missed. Results are stored server-side either way.