Agent Integration
Your agent backend now receives requests from Convoy instead of directly from your client. You need to do two things:
- Verify the request — check Convoy’s HMAC signature
- Report metrics — send latency, outcome, cost, and input/output to Convoy
1. Verify the request
Convoy signs every request it forwards using HMAC-SHA256 with your shared secret. Your agent should verify this signature to confirm the request came from Convoy.
| Header | Description |
|---|
X-Convoy-Signature | HMAC signature: t={timestamp},v1={hmac} |
X-Convoy-Agent-Name | Your agent’s name |
X-Convoy-Version-Name | Which version this request was routed to (e.g. v1, v2-test) |
Session-ID | The session ID from the client |
How to verify
- Parse
X-Convoy-Signature to extract t (timestamp), v1 (HMAC), and optionally v2 (HMAC)
- Reject if timestamp is older than 5 minutes
- Build the signed payload:
{timestamp}\n{METHOD}\n{path_with_query}\n{body}
- Compute HMAC-SHA256 using your secret
- Accept if either
v1 or v2 matches
import hmac
import hashlib
import time
def verify_convoy_signature(signature_header: str, secret: str,
method: str, path: str, body: str):
"""Verify HMAC signature from Convoy proxy."""
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > 300:
raise ValueError("Signature expired")
payload = f"{timestamp}\n{method.upper()}\n{path}\n{body}"
expected = hmac.new(
secret.encode(), payload.encode(), hashlib.sha256
).hexdigest()
# v1 = current secret, v2 = previous secret (during rotation)
# Your env has one secret — it will match whichever version was signed with it
for v in ["v1", "v2"]:
if v in parts and hmac.compare_digest(parts[v], expected):
return
raise ValueError("Invalid signature")
During secret rotation, Convoy sends both v1 (signed with the new secret) and v2 (signed with the old secret) for 24 hours. Your env has one secret — it will match one of them regardless of whether you’ve rotated yet. The code above handles this automatically.
2. Report metrics
After your agent processes the request, report metrics to Convoy. This data powers test evaluation — Convoy uses it to decide whether to promote or roll back.
Endpoint: POST https://api.convoylabs.com/v1/ingest/sessions
Auth: Authorization: Bearer <CONVOY_SECRET> (same secret)
Payload
| Field | Type | Required | Description |
|---|
session_id | string | always | From the Session-ID header |
agent_name | string | always | From the X-Convoy-Agent-Name header |
version_name | string | always | From the X-Convoy-Version-Name header |
outcome | "success" | "error" | always | Did this step succeed or fail? |
is_last_step | boolean | always | Is this the final step of the session? |
latency_ms | integer | always | How long the agent took (milliseconds) |
cost_usd | float | always | Token/API cost in USD (0 if none) |
input | string | when is_last_step | The input to the agent |
output | string | when is_last_step | The agent’s response — everything you want the judge to evaluate |
step_index | integer | optional | Zero-based step index in the session (default: 0) |
attempt_number | integer | optional | Attempt number for this step, 1-based (default: 1) |
Rules
output is only allowed when is_last_step is true
- When
is_last_step is true, both input and output are required
output should contain everything you want the judge prompt to evaluate — typically the agent’s response, and optionally reasoning or tool calls
- For multi-step sessions (conversational agents, workflows), increment
step_index per step (0, 1, 2…) and report the final step with is_last_step: true
- If your agent retries a step, report each attempt with the same
step_index but increment attempt_number (1, 2, 3…). All attempts are recorded
Code examples
import httpx
import time
import os
async def handle_agent_request(request):
# 1. Verify signature (see above)
verify_convoy_signature(
request.headers["X-Convoy-Signature"],
os.environ["CONVOY_SECRET"],
request.method,
request.url.path,
await request.body(),
)
# 2. Extract Convoy headers
session_id = request.headers["Session-ID"]
agent_name = request.headers["X-Convoy-Agent-Name"]
version_name = request.headers["X-Convoy-Version-Name"]
# 3. Run your agent and measure
start = time.time()
result = await run_agent(request) # your agent logic
latency_ms = int((time.time() - start) * 1000)
# 4. Report metrics (fire-and-forget to not block the response)
payload = {
"session_id": session_id,
"agent_name": agent_name,
"version_name": version_name,
"input": result.input,
"outcome": "success" if result.ok else "error",
"is_last_step": result.is_done,
"latency_ms": latency_ms,
"cost_usd": result.token_cost,
"step_index": result.step_index, # optional, default 0
"attempt_number": result.attempt_number, # optional, default 1
}
if result.is_done:
payload["output"] = result.response
async with httpx.AsyncClient() as client:
await client.post(
"https://api.convoylabs.com/v1/ingest/sessions",
headers={"Authorization": f"Bearer {os.environ['CONVOY_SECRET']}"},
json=payload,
)
# 5. Return response to user
return result.response
Report metrics asynchronously (fire-and-forget) so it doesn’t block your agent’s response to the user. Reporting the same (session_id, step_index, attempt_number) twice overwrites the previous data — safe to retry on network errors. For production, add retries with exponential backoff on 5xx and timeout errors to avoid losing metric data.