Skip to Content
IntegrationsAWS S3 Object Delivery

AWS S3 Object Delivery

Write the complete cargo result — no size cap — as a JSON object in your S3 bucket. Every other direct-AWS target inlines at most ~240 KB and falls back to a pointer for oversize results; S3 delivery ships the full response every time. And it doubles as a trigger for free: attach S3 Event Notifications (→ Lambda / SQS / EventBridge) to the prefix.

your code ── POST /cargo/load ──▶ Convoy ──▶ …batch processing… │ ▼ cargo completes AssumeRole(your role, ExternalId) │ ▼ s3:PutObject(Bucket: your-bucket, Key: {key_prefix}{cargo_id}.json, Body: <FULL result envelope>) │ ▼ S3 Event Notification on the prefix → Lambda / SQS / EventBridge (or your pipeline just reads the object)

Why S3 delivery:

  • No size cap — the object always carries the complete response; response_truncated is always false. This is the target for large results.
  • You own the data at rest — your bucket, your retention (lifecycle rules), your encryption, your access control, your data-residency story.
  • Exactly-once effective — the object key is deterministic ({key_prefix}{cargo_id}.json), so a delivery retry simply overwrites the same object with identical bytes. (Buckets with versioning enabled will record a duplicate version — harmless.)
  • Write-only, prefix-scoped — Convoy only ever gets s3:PutObject scoped to your agreed prefix. It can never read or list your bucket, and it cannot write outside the prefix even if your role policy is bucket-wide (each delivery session carries a prefix-scoped session policy on top of your role policy).

Object shape

PropertyValue
Key{key_prefix}{cargo_id}.json (default prefix convoy/)
Content-Typeapplication/json
Bodythe standard result envelope — full response, never truncated
Metadata convoy-cargo-idthe cargo ID
Metadata convoy-successtrue | false

The object metadata lets event-notification consumers and lifecycle tooling filter without reading the body.

S3 bucket ARNs are region-less, so this is the one target with no region check against your connection: Convoy resolves the bucket’s actual region at delivery time (and caches it). Your bucket can live in any region — the full result lands wherever the bucket lives, which is a feature for data-residency requirements.

Setup

Create an AWS connection

Same connection as the other direct-AWS targets — see AWS connections. One connection can carry every capability.

Grant s3:PutObject on your role — prefix-scoped

Scope it to the bucket AND prefix — never the whole bucket unless you mean it, and never Resource: "*":

{ "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::acme-cargo-results/convoy/*" }

Or use the CloudFormation role template  with the S3BucketPrefixArns parameter (you supply the already-prefix- scoped resource string — the template never widens it).

If the bucket default-encrypts with a customer-managed KMS key (SSE-KMS), also grant the role kms:GenerateDataKey on that key — standard S3-writer setup.

Verify the capability (optional)

In the dashboard, open your AWS connection and run the S3 capability check with your bucket ARN and key prefix.

PutObject has no dry-run mode, so the s3 probe writes one real marked object — {key_prefix}convoy-verify.json with body {"convoy_verification": true}. The key is deterministic, so repeated probes overwrite the same tiny object (no accumulation); you can lifecycle-expire or event-filter the convoy-verify.json key.

Side-effectful probes are rate-capped at 1/minute per connection, and probes are opt-in per capability — fail-at-delivery is a valid choice.

Submit cargo with the aws_s3 callback

curl -X POST https://api.cnvy.ai/cargo/load \ -H "X-API-Key: $CONVOY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "params": { "model": "claude-haiku-4-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Summarize this document…"}] }, "callback": { "type": "aws_s3", "connection_id": "awsconn_…", "bucket_arn": "arn:aws:s3:::acme-cargo-results", "key_prefix": "convoy/results/" }, "metadata": {"job_id": "batch-2026-08-29"} }'
  • bucket_arn — arn:aws:s3:::bucket only, no key path. When your connection carries a target allow-list, the ARN must be listed on it.
  • key_prefix — optional, defaults to convoy/. Must end with / when non-empty; no .. segments, no leading / (422 invalid_key_prefix otherwise). The result lands at {key_prefix}{cargo_id}.json.

Triggering on delivery — S3 Event Notifications

Attach a notification to the prefix so an object write fires your consumer:

# SAM/CloudFormation — bucket notification → Lambda ResultsBucket: Type: AWS::S3::Bucket Properties: BucketName: acme-cargo-results NotificationConfiguration: LambdaConfigurations: - Event: s3:ObjectCreated:Put Filter: S3Key: Rules: - Name: prefix Value: convoy/results/ - Name: suffix Value: .json Function: !GetAtt OnCargoResult.Arn

Your consumer reads the object (or just its convoy-* metadata) and processes the full result:

import boto3, json s3 = boto3.client("s3") def handler(event, context): for record in event["Records"]: bucket = record["s3"]["bucket"]["name"] key = record["s3"]["object"]["key"] # Skip the verification probe object. if key.endswith("convoy-verify.json"): continue head = s3.head_object(Bucket=bucket, Key=key) if head["Metadata"].get("convoy-success") != "true": handle_failure(bucket, key) continue envelope = json.loads( s3.get_object(Bucket=bucket, Key=key)["Body"].read() ) process(envelope["cargo_id"], envelope["response"], envelope["metadata"])

Lifecycle hygiene: add a lifecycle rule on the prefix so old results expire on your schedule — result retention in your bucket is entirely yours (the hosted results mailbox keeps its own independent TTL).

The result envelope

The object body is the same envelope every direct-AWS target uses — except response is always fully inlined here:

FieldNotes
cargo_idmatches the object key’s {cargo_id}
successtrue | false (also in object metadata)
responsethe FULL model response — never truncated for S3 delivery
response_truncatedalways false for S3 delivery
result_urlhosted-mailbox URL (consistent with other targets; the object already has everything)
metadatayour submit-time metadata, echoed verbatim
error_messagepresent only when success is false

Failure cargo

There is no separate failure API — failed cargo writes the same deterministic key with success: false + error_message in the body and convoy-success: false in the object metadata, so consumers can branch on the metadata without reading the body.

Troubleshooting

SymptomCause / fix
422 invalid_key_prefix at submitPrefix has a leading /, a .. segment, a disallowed character, or a missing trailing / — fix the key_prefix.
422 target_not_allowed at submitThe bucket ARN isn’t on the connection’s target allow-list.
Delivery fails target_not_foundThe bucket was deleted since submit.
Delivery fails assume_role_denieds3:PutObject grant removed, a bucket policy Deny, or a broken trust policy — the connection is flagged failed and your org is emailed; fix the role and re-verify.
Delivery fails target_misconfigured (KMS.*)The bucket default-encrypts with a customer-managed key the role can’t use — grant kms:GenerateDataKey on that key.
Repeated SlowDown retriesS3 prefix throttling — Convoy’s backoff handles it; consider spreading keys across prefixes at very high volumes.
Duplicate object versionsVersioned bucket + a delivery retry — the versions carry identical bytes; harmless.
Last updated on