AWS Lambda Invoke
Have Convoy invoke your Lambda function with the result when a cargo completes — no webhook endpoint, no polling. From your side, the function simply “fires when the cargo is done”, receiving the standard result envelope as its event.
your code ── POST /cargo/load ──▶ Convoy ──▶ …batch processing…
│
▼ cargo completes
AssumeRole(your role, ExternalId)
│
▼
lambda:InvokeFunction(InvocationType='Event',
Payload=<result envelope>)
│
▼
your function runs with the resultConvoy assumes the IAM role from your AWS connection
and calls lambda:InvokeFunction asynchronously (InvocationType='Event').
Convoy’s delivery responsibility ends at the 202 from the Lambda
front-end — after that, Lambda’s own async invoke machinery owns
execution: its per-function retries (2 by default) and your configured
on-failure destination
or DLQ.
This differs from the token-based targets (durable callbacks and Step Functions task tokens): there is no suspended wait to resume. It’s the right fit when you want an event-driven handler for results rather than a paused workflow.
Setup
Create an AWS connection
Same connection as the other direct-AWS targets — see AWS connections. One connection can carry every capability.
When you create the connection, list the function(s) Convoy may invoke in
allowed_function_arns. For aws_lambda_invoke this list is enforced
at submit time: a non-null list rejects any function_arn not on it.
Grant lambda:InvokeFunction on your role
Add an invoke statement to the connection’s IAM role, scoped to your exact
function(s) — never Resource: "*":
{
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": [
"arn:aws:lambda:us-east-1:123456789012:function:on-cargo-done",
"arn:aws:lambda:us-east-1:123456789012:function:on-cargo-done:*"
]
}Or use the CloudFormation role template
with the InvokeFunctionArns parameter.
Convoy defends in depth: each delivery’s STS session carries a session policy scoped to the ONE function the cargo named, so even a broader role policy can’t be used to invoke anything else.
Verify the capability
In the dashboard, open your AWS connection and run the Lambda invoke
capability check with your function’s qualified ARN (e.g.
arn:aws:lambda:us-east-1:123456789012:function:on-cargo-done:$LATEST).
The probe DryRun-invokes your function — it validates the permission
without executing it.
Submit cargo with the invoke callback
The function_arn must be a qualified ARN — append :$LATEST, a
version number, or an alias. Unqualified ARNs are rejected at submit
(durable functions cannot be invoked with an unqualified ARN).
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_lambda_invoke",
"connection_id": "awsconn_…",
"function_arn": "arn:aws:lambda:us-east-1:123456789012:function:on-cargo-done:$LATEST"
}
}'Submit-time validation: the ARN must be qualified (version, alias, or
$LATEST), must match the connection’s region (region_mismatch
otherwise), and — when the connection has an allowed_function_arns list —
must be on it (target_not_allowed otherwise). A stored unqualified ARN on
the allow-list also matches qualified submits of the same function
(…:function:name allows …:function:name:prod).
The handler
Your function receives the result envelope directly as event:
def handler(event, context):
if event["success"]:
response = event["response"]
if response is None and event["response_truncated"]:
# Oversize result — fetch from the hosted results mailbox
response = fetch(event["result_url"])
process(response)
else:
# Failure cargo delivers through the SAME invoke, with
# success=false and an error_message — there is no separate
# failure API for this target.
handle_failure(event["cargo_id"], event["error_message"])Envelope fields:
| Field | Notes |
|---|---|
cargo_id | The cargo identifier — dedupe key (see below) |
success | true/false |
response | Full model response (null on failure or when truncated) |
response_truncated | true when the response exceeded the 240 KB inline budget |
result_url | GET /cargo/{cargo_id}/result — always present |
error_message | Present only on failure envelopes |
metadata | Your opaque /cargo/load metadata, echoed verbatim |
Async invoke accepts payloads up to 256 KB; Convoy’s 240 KB inline budget
keeps every envelope under it — oversize responses ship the
result_url pointer instead.
At-least-once: dedupe on cargo_id
InvokeFunction(Event) is not idempotent. If Convoy’s delivery times
out after Lambda accepted the invoke but before the 202 arrived, the
delivery retries and your function may run twice (Lambda’s own async
retries can also re-run it). If duplicate processing matters, dedupe on
cargo_id — one DynamoDB conditional put:
import boto3
from botocore.exceptions import ClientError
table = boto3.resource("dynamodb").Table("processed-cargo")
def handler(event, context):
try:
table.put_item(
Item={"cargo_id": event["cargo_id"]},
ConditionExpression="attribute_not_exists(cargo_id)",
)
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return # already processed — duplicate delivery
raise
process(event)Configure an on-failure destination. A 202 means Lambda accepted the
invoke — if your function then crashes on every retry, Convoy considers
the cargo delivered (that’s the async-invoke contract). Set an
OnFailure destination (SQS/SNS/EventBridge) on the function so failed
executions land somewhere you can see. The result_url in the envelope
means the result itself is never lost — it stays retrievable from the
hosted results mailbox for the retention period.
Delivery semantics
- Both success and failure cargo invoke your function — branch on
event["success"]. - Retries: transient AWS errors (throttles, 5xx) retry up to 5 times
with exponential backoff (1, 3, 9, then 27 minutes between attempts —
roughly 40 minutes end to end). Terminal conditions
(function deleted, invoke grant removed, broken function KMS key) stop
immediately and mark the cargo
callback_failed. - Recovery: the result stays retrievable via
GET /cargo/{cargo_id}/resultregardless of delivery outcome.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
422 region_mismatch at submit | function_arn region ≠ connection region | Use a connection registered for the function’s region |
| 422 unqualified ARN at submit | function_arn missing :$LATEST/version/alias | Use a qualified function ARN |
422 target_not_allowed at submit | ARN not on allowed_function_arns | Add it to the connection’s allow-list (or clear the list) |
target_not_found in delivery logs | Function deleted/renamed since submit | Redeploy the function or resend with the new ARN |
assume_role_denied | Trust policy or invoke grant removed | Fix the role, re-verify the connection |
target_misconfigured (KMS…) | Function’s env-var KMS key broken | Fix the key customer-side; resend |
| Function ran twice | At-least-once delivery | Dedupe on cargo_id (above) |
| Function never ran but cargo shows delivered | Function crashed after the 202 | Check the function’s own logs + OnFailure destination |