Delivery targets & retention
BatchRouter gives you three ways to get results out of a completed batch, and one policy that controls how long those results stay available. This guide covers when to use each delivery mode, how to register and test a delivery target (a customer-owned S3-compatible bucket), how to re-deliver a batch whose delivery failed, and how to set your retention policy.
Delivery options
Section titled “Delivery options”Every batch produces a results artifact once it reaches completed. How you receive it is up to you:
| Mode | How it works | Best for |
|---|---|---|
| Poll (default) | Call GET /v1/batches/{id} until terminal, then read GET /v1/batches/{id}/results (paginated) or GET /v1/batches/{id}/artifact-url (signed URL). Nothing to configure. | Scripts, cron jobs, anything that can wait and ask. |
| Webhook | BatchRouter POSTs a signed event to your URL when the batch finishes. Set per-batch (webhook: { url, secret }) or org-wide with PUT /v1/auth/account/delivery-webhook. | Event-driven pipelines that react on completion. |
| Delivery target | BatchRouter writes the results artifact directly into your own S3-compatible bucket. | Keeping outputs inside your own storage/VPC, large-volume archival, no egress through your app. |
These are not mutually exclusive — you can poll and receive a webhook, or have a delivery target and still fetch results over the API.
Delivery targets (customer buckets)
Section titled “Delivery targets (customer buckets)”A delivery target is an S3-compatible bucket you own (AWS S3, Cloudflare R2, MinIO, or any S3-API service). When a batch with a target reaches completed, BatchRouter writes the results artifact into that bucket under the target’s prefix.
Register a target
Section titled “Register a target”POST /v1/delivery-targets registers a bucket. The secret_access_key is write-only — it’s stored encrypted and is never returned by any endpoint. The endpoint must be a public HTTPS URL; internal and metadata hosts are rejected.
Required fields: endpoint, region, bucket, access_key_id, secret_access_key. Optional: prefix, verify_before_gc (default true), is_default (default false).
curl -X POST https://api.batchrouter.com/v1/delivery-targets \ -H "Authorization: Bearer $BATCHROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "https://s3.us-east-1.amazonaws.com", "region": "us-east-1", "bucket": "my-batch-results", "prefix": "batchrouter/", "access_key_id": "AKIAEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/EXAMPLEKEY", "is_default": true }'const res = await fetch("https://api.batchrouter.com/v1/delivery-targets", { method: "POST", headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ endpoint: "https://s3.us-east-1.amazonaws.com", region: "us-east-1", bucket: "my-batch-results", prefix: "batchrouter/", access_key_id: "AKIAEXAMPLE", secret_access_key: "wJalrXUtnFEMI/EXAMPLEKEY", is_default: true, }),});
const { delivery_target } = await res.json();console.log(delivery_target.id);import os, requests
res = requests.post( "https://api.batchrouter.com/v1/delivery-targets", headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"}, json={ "endpoint": "https://s3.us-east-1.amazonaws.com", "region": "us-east-1", "bucket": "my-batch-results", "prefix": "batchrouter/", "access_key_id": "AKIAEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/EXAMPLEKEY", "is_default": True, },)
target = res.json()["delivery_target"]print(target["id"])A 201 returns the created delivery_target (id, kind, endpoint, region, bucket, prefix, access_key_id, verify_before_gc, is_default, timestamps) — never the secret. Setting is_default: true makes this the target used for batches that don’t name one explicitly.
GET /v1/delivery-targets lists your configured targets (no secrets). DELETE /v1/delivery-targets/{id} removes one.
Test connectivity
Section titled “Test connectivity”Before relying on a target, run a connectivity probe. POST /v1/delivery-targets/{id}/test performs a tiny write-then-delete against the bucket — because delivery uses PUT, a read-only credential will fail the probe, which is intentional.
curl -X POST https://api.batchrouter.com/v1/delivery-targets/$TARGET_ID/test \ -H "Authorization: Bearer $BATCHROUTER_API_KEY"const res = await fetch( `https://api.batchrouter.com/v1/delivery-targets/${targetId}/test`, { method: "POST", headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}` }, },);
const { test } = await res.json();if (!test.ok) console.error(test.error.code, test.error.message);import os, requests
res = requests.post( f"https://api.batchrouter.com/v1/delivery-targets/{target_id}/test", headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"},)
test = res.json()["test"]if not test["ok"]: print(test["error"]["code"], test["error"]["message"])The response is { "test": { "ok": <bool>, "checked": "write", "error"?: { code, message } } }. On failure, error.code classifies the problem so you can fix it precisely:
| Code | Meaning |
|---|---|
credential_error | The access key / secret pair was rejected. |
permission_denied | The credential can’t write to the bucket/prefix. |
bucket_not_found | The bucket doesn’t exist at this endpoint/region. |
bucket_error | The bucket exists but the write failed for another reason. |
unreachable | The endpoint couldn’t be reached. |
Re-deliver a failed batch
Section titled “Re-deliver a failed batch”If a batch completed but its delivery to your bucket failed (an expired credential, a transient outage), re-enqueue it with POST /v1/batches/{batchId}/redeliver.
curl -X POST https://api.batchrouter.com/v1/batches/$BATCH_ID/redeliver \ -H "Authorization: Bearer $BATCHROUTER_API_KEY"const res = await fetch( `https://api.batchrouter.com/v1/batches/${batchId}/redeliver`, { method: "POST", headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}` }, },);
const batch = await res.json(); // 202 — re-delivery enqueuedconsole.log(batch.delivery?.status);import os, requests
res = requests.post( f"https://api.batchrouter.com/v1/batches/{batch_id}/redeliver", headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"},)
batch = res.json() # 202 — re-delivery enqueuedprint(batch["delivery"]["status"])A 202 re-enqueues delivery. The call is guarded against double-delivery via the batch’s delivery_status: it returns 409 if the batch is already delivered or delivering, or if customer-bucket delivery isn’t enabled / the batch has no configured target.
Result retention
Section titled “Result retention”Retention controls how long BatchRouter keeps a batch’s results before garbage-collecting the stored bytes. It’s an org-wide policy you read and set via GET|PUT /v1/retention-settings. The default is the free grace_only tier.
| Tier | What it keeps | Cost |
|---|---|---|
grace_only (default) | Results for a short free grace window after completion, then GC. | Free |
extended_fixed | Results for a fixed extended_days (1–3650). | Metered storage charge |
until_deleted | Results until you delete them. | Metered storage charge |
Read the current policy
Section titled “Read the current policy”curl https://api.batchrouter.com/v1/retention-settings \ -H "Authorization: Bearer $BATCHROUTER_API_KEY"const res = await fetch("https://api.batchrouter.com/v1/retention-settings", { headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}` },});const { retention_settings } = await res.json();console.log(retention_settings.retention_tier);import os, requests
res = requests.get( "https://api.batchrouter.com/v1/retention-settings", headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"},)print(res.json()["retention_settings"]["retention_tier"])Set the policy
Section titled “Set the policy”retention_tier is the only required field. The example below keeps results for 90 days.
curl -X PUT https://api.batchrouter.com/v1/retention-settings \ -H "Authorization: Bearer $BATCHROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "retention_tier": "extended_fixed", "extended_days": 90, "fee_acknowledged": true }'const res = await fetch("https://api.batchrouter.com/v1/retention-settings", { method: "PUT", headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ retention_tier: "extended_fixed", extended_days: 90, fee_acknowledged: true, }),});const { retention_settings } = await res.json();console.log(retention_settings);import os, requests
res = requests.put( "https://api.batchrouter.com/v1/retention-settings", headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"}, json={ "retention_tier": "extended_fixed", "extended_days": 90, "fee_acknowledged": True, },)print(res.json()["retention_settings"])