Activity Logging
Write every call, voicemail, and recording into your system of record.
This is a pattern built on Webhook Events, not a separate API — webhooks are the right transport because they give you durable retries, signed payloads, and stable event IDs. The pattern is the same regardless of your stack: receive → dedupe → write.
The four primary event types
These four cover the minimum needed to reconstruct a call log. call.end carries everything you need to write the row — direction, timestamps, duration, status, caller/callee — so you don't need call.start for logging. Subscribe to call.start or call.incoming additionally if you want a pre-answer row (e.g. to show ringing calls in a live dashboard).
| Event | When it fires | What to persist |
|---|---|---|
call.end | Call completes (answered, no-answer, busy, or failed) | A row in your call log — timestamps, duration, direction, status |
voicemail.new | A voicemail is left | A row in your voicemail log with the audio URL and caller info |
recording.available | A call recording finishes | The recording URL, attached to the existing call log row |
recording.transcription.complete / voicemail.transcription.complete | Transcripts become ready | The transcript text, attached to the call or voicemail row |
See Webhook Events for every field in every payload.
Minimal handler
// POST /webhooks/dialstack
app.post('/webhooks/dialstack', express.raw({ type: 'application/json' }), async (req, res) => {
verifySignature(req); // see webhook-events.md
const event = JSON.parse(req.body);
// Dedupe by event.id — DialStack may deliver the same event more than once.
if (await db.processedEvents.exists(event.id)) {
return res.status(200).end();
}
await db.processedEvents.insert({ id: event.id });
switch (event.type) {
case 'call.end':
await db.calls.upsert({
call_id: event.data.call_id,
account_id: event.account_id,
direction: event.data.direction,
from: event.data.from_number,
to: event.data.to_number,
user_id: event.data.user_id,
status: event.data.status,
started_at: event.data.started_at,
answered_at: event.data.answered_at,
ended_at: event.data.ended_at,
duration_seconds: event.data.duration_seconds,
});
break;
case 'recording.available':
await db.calls.update(event.data.call_id, {
recording_url: event.data.url,
});
break;
case 'recording.transcription.complete':
await db.calls.update(event.data.call_id, {
transcript: event.data.transcript,
});
break;
case 'voicemail.new':
await db.voicemails.insert({
voicemail_id: event.data.voicemail_id,
account_id: event.account_id,
from: event.data.from_number,
audio_url: event.data.url,
received_at: event.created_at,
});
break;
case 'voicemail.transcription.complete':
await db.voicemails.update(event.data.voicemail_id, {
transcript: event.data.transcript,
});
break;
}
res.status(200).end();
});
Respond 200 fast. If any persist step is slow, queue it (SQS, BullMQ) and let the webhook return immediately — DialStack will treat anything slower than a few seconds as a failure and retry.
Retry behavior
DialStack retries non-2xx responses with exponential backoff. That means:
- Dedupe on
event.id. Every event carries a stable ID. A retry of the same event reuses that ID, so if you key your writes on it (or record the IDs you've already processed), retries won't produce duplicate rows. - Accept out-of-order delivery.
recording.availablecan arrive afterrecording.transcription.completeif the transcript came from an earlier attempt. Persist what you have, enrich what comes later. - Short timeouts. DialStack's own timeout is tight; don't make upstream calls on the webhook path.
Enrichment window
call.end fires immediately when the call ends. Recordings and transcripts arrive later:
recording.available— seconds to a minute aftercall.end.*.transcription.complete— typically under a minute for short calls; longer for long calls.
Your UI should show the call row immediately on call.end and progressively enrich it as the other events arrive.
See also
- Webhook Events — full event surface + signature verification.
- Screen Pop — the other half of the event loop: react to calls as they ring.
- Appointment Webhooks — request/response webhooks for AI Scheduling.