Post-Call Transcripts to Your Agent
After every call ends, AgentCall POSTs the full transcript and an LLM-extracted summary to your endpoint. Your agent platform reads the outcome and acts on it: drafts follow-up emails, schedules meetings, updates context for the next call. This is the loop that turns a phone call into something your AI employee can remember.
The Post-Call Loop
A live phone call between you and your AI agent produces a transcript. Without a post-call webhook, that transcript dies in the AgentCall dashboard. With one, your agent platform absorbs it and can take real actions based on what was decided. Same payload powers any pattern: store and analyze later, fire off emails, push to a CRM, update tomorrow's brief.
call.transcript event, signed with HMAC-SHA256 in the X-AgentCall-Signature header. Failed deliveries are retried automatically.Payload Schema
Every webhook delivery from AgentCall is wrapped in a standard envelope. The call.transcript event-specific fields live inside the data object. Make sure your parser reads body.data.callId, not body.callId.
{
"event": "call.transcript",
"timestamp": "2026-05-13T14:30:15.000Z",
"data": {
"callId": "call_xxx",
"duration": 53,
"transcript": [
{ "role": "ai", "text": "Hey, what do you want to dig into?", "timestamp": "2026-05-13T14:30:00.000Z" },
{ "role": "human", "text": "Walk me through this morning.", "timestamp": "2026-05-13T14:30:04.123Z" },
{ "role": "ai", "text": "Three updates from your inbox...", "timestamp": "2026-05-13T14:30:07.456Z" }
],
"summary": {
"summary": "Caller wanted a walkthrough of the morning brief and decided to follow up with Sarah at Acme on Wednesday.",
"callerName": "David",
"intent": "scheduling",
"urgency": "medium",
"callbackBy": "Wednesday 2 PM CT",
"spam": false
}
}
}callIdStable ID. Same value across the call.transcript webhook, GET /v1/calls/:callId/transcript, and dashboard.
durationSeconds, measured by the media bridge clock (not the carrier's).
transcriptArray of turns. role is ai or human. Timestamps are ISO 8601 UTC.
summary.intentOne of: service_request, quote_request, scheduling, complaint, spam, general_inquiry, other.
summary.urgencyOne of high, medium, low. high means the caller said urgent, today, ASAP, or emergency.
summary.spamWhen true, the LLM thinks this is a telemarketer, robocall, wrong number, or hostile caller. Use it to skip downstream actions.
Subscribe
Register a webhook
Pick one of three ways. They all produce the same webhook record:
Via MCP (talking to your agent, e.g. Hermes or Claude Desktop):
“Register an AgentCall webhook for call.transcript events pointing at https://my-agent.example.com/agentcall/transcript.”
Your agent calls create_webhook on the AgentCall MCP server. The response includes the signing secret, which is only shown once. Save it.
Via the Node SDK:
import { AgentCall } from 'agentcall'
const client = new AgentCall({ apiKey: process.env.AGENTCALL_API_KEY })
const wh = await client.webhooks.create({
url: 'https://my-agent.example.com/agentcall/transcript',
events: ['call.transcript'],
})
console.log(wh.secret) // shown once, store itVia REST:
curl -X POST https://api.agentcall.co/v1/webhooks \
-H "Authorization: Bearer ac_live_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://my-agent.example.com/agentcall/transcript",
"events": ["call.transcript"]
}'Verify the HMAC signature
Every delivery carries an X-AgentCall-Signature header of the form sha256=<hex>. Compute HMAC-SHA256 of the raw request body using the signing secret you saved in Step 1, hex-encode it, prefix with sha256=, and compare with constant-time equality. Reject anything that doesn't match.
import express from 'express'
import crypto from 'node:crypto'
const app = express()
const SECRET = process.env.AGENTCALL_WEBHOOK_SECRET! // from Step 1
// IMPORTANT: keep the raw body for HMAC. Standard JSON parsing breaks it.
app.post(
'/agentcall/transcript',
express.raw({ type: 'application/json' }),
(req, res) => {
const provided = req.header('X-AgentCall-Signature') || ''
if (!provided.startsWith('sha256=')) return res.status(401).end()
const expected =
'sha256=' +
crypto.createHmac('sha256', SECRET).update(req.body).digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(provided)
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).end()
}
const body = JSON.parse(req.body.toString('utf8'))
// Envelope: { event, timestamp, data: { callId, transcript, summary, ... } }
const { callId, transcript, summary } = body.data
// feed callId / transcript / summary into your agent
res.status(200).end()
},
)export default {
async fetch(req: Request, env: { SECRET: string }) {
const body = await req.text()
const provided = req.headers.get('X-AgentCall-Signature') || ''
if (!provided.startsWith('sha256=')) return new Response('unauthorized', { status: 401 })
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(env.SECRET),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
)
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body))
const expected =
'sha256=' +
Array.from(new Uint8Array(sig))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
let diff = expected.length === provided.length ? 0 : 1
for (let i = 0; i < expected.length && i < provided.length; i++) {
diff |= expected.charCodeAt(i) ^ provided.charCodeAt(i)
}
if (diff !== 0) return new Response('unauthorized', { status: 401 })
const parsed = JSON.parse(body)
// Envelope: { event, timestamp, data: { callId, transcript, summary, ... } }
const { callId, transcript, summary } = parsed.data
return new Response('ok')
},
}Process the transcript
Three patterns customers run, pick whichever fits:
Pattern A: store and analyze later
Write the payload to a database row keyed by callId. Cheapest. Good if your agent reads transcripts on its own schedule rather than reacting immediately.
Pattern B: forward to your agent in real time
POST the payload to your agent platform's ingest endpoint. The agent acts on the call before the caller is back at their desk. Best for the “agent answers, then executes” loop.
Pattern C: queue for a local agent to poll
If your agent runs locally (Docker on a VPS, on your laptop) and can't accept inbound HTTPS, host a tiny always-on bridge that queues transcripts. The agent polls on cron and drains the queue. The reference bridge below ships this pattern out of the box.
Reference Bridge for Local Agents (Pattern C)
If your agent runs locally and can't expose a public HTTPS URL, the same Cloudflare Worker that handles pre-call context also queues post-call transcripts (v0.2.0+). Two endpoints sit on the bridge alongside the pre-call handlers:
POST /agentcall/transcriptreceives the webhook from AgentCall, verifies the signature, and appends to a bounded queue in KV (max 100 entries, oldest dropped if exceeded). The bridge also dedups bycallIdon every write, so if AgentCall retries a delivery the same call never lands in the queue twice.POST /hermes/pull-transcriptslets your local agent drain the queue. Returns all queued entries and atomically clears them. Protected by the sameX-Hermes-Push-Keyheader.
Source is open at Kintupercy/agentcall-hermes-bridge. Already deployed your bridge for the pre-call context flow? You only need to redeploy with the new code; the KV namespace, secrets, and custom domain stay the same.
# cron entry or background loop
curl -X POST https://hermes.your-domain.com/hermes/pull-transcripts \
-H "X-Hermes-Push-Key: $HERMES_PUSH_KEY" \
| jq '.transcripts[]' \
| your-agent ingest-transcriptThe pull endpoint is read-and-clear: anything you don't persist locally before the next pull is gone. Persist before you process.
Heads up for Python-based agents
Cloudflare blocks Python's default urllib User-Agent on this bridge with HTTP 403 (Error 1010, browser_signature_banned). If your local agent runs Python and pulls via urllib, every cron tick gets silently rejected at the Cloudflare edge before reaching the Worker. Fix: shell out to curl via subprocess.run(['curl', ...]), or set a non-default header like User-Agent: curl/8.0 on your urllib requests. Node, Go, Rust, and curl-based clients are unaffected. Check Cloudflare Security Events for Error 1010 if the queue depth on the bridge keeps climbing while your agent reports successful pulls.
Retries and Reliability
Retry policy: AgentCall retries failed deliveries with exponential backoff. A “failure” is any non-2xx response or a connection error.
Acknowledge fast: return 2xx as soon as you've verified the signature and persisted the payload. Heavy processing belongs in a background job, not inline. AgentCall's delivery worker times out long requests.
Idempotency: callId is stable across retries. Use it as the dedup key so a redelivered transcript doesn't double-fire your agent actions.
Rotate secrets: if the signing secret leaks, call webhooks.rotateSecret(webhookId) on the SDK or ask your agent to do it. The old secret is invalidated immediately; the new one is shown once.
Troubleshooting
Webhook fires but signature always invalid
Most often the request body was JSON-parsed before HMAC. Standard middleware re-serializes JSON and changes whitespace, breaking the signature. Use raw body for the signature check, then JSON-parse afterward. The Express example above uses express.raw for this reason.
Some calls don't produce a webhook
The webhook only fires for calls handled by inbound AI. Direct-to-voicemail or unanswered calls don't produce a transcript. The call.status webhook covers those.
Endpoint receives the payload but my agent already acted
Duplicate delivery. Always dedupe on callId: store the IDs you've processed and skip repeats. AgentCall retries until it gets a 2xx, so a slow first response can produce a duplicate.
Transcript is shorter than the call
Transcription captures turns the AI and the caller exchanged. Long stretches of silence, music on hold, or one side muted produce gaps. If you need the raw audio, subscribe to call.recording as well.
Reference
Ready to close the post-call loop?
Get an AgentCall API key, register a webhook for call.transcript, and start feeding outcomes back into your agent on every call.