Call Bullet

Show call logs in an AI-built dashboard

To show call logs in an AI-built dashboard, call Call Bullet's GET /workspaces/{id}/calls from your server with calls:read, then render caller, time, answered or missed, duration and source. Sync changes with updated_since, and open recordings through short-lived links from GET /calls/{id}/recording.

Last updated · Call Bullet

What does a call log need to show?

A useful call log answers four questions at a glance: who rang, when, did anyone answer, and where they came from. Everything else, such as recordings and transcripts, sits one click away.

Most dashboards show the newest calls first, with missed calls highlighted so someone rings back. Times arrive in UTC and should be shown in UK time.

ColumnFieldNotes
Date and timestarted_atConvert to Europe/London
Callerfrom_numberE.164; match to your contacts
Number rungto_numberWhich tracking number
Statusanswered, call_statusMissed calls need follow-up
Durationduration_secondsShow as m:ss
Sourcesource / mediumWith include=attribution
First-time calleris_first_time_callerNew enquiry badge
Lead statuslead_statusnew, qualified, booked…

How do I fetch the calls?

One server function returns a page of calls with attribution included.

TypeScript · list calls
import { cb } from "./call-bullet.server";

export type CbCall = {
  id: string; started_at: string; from_number: string; to_number: string;
  answered: boolean; call_status: string; duration_seconds: number;
  source: string | null; medium: string | null; is_first_time_caller: boolean;
  has_recording: boolean; lead_status: string | null;
};

export async function listCalls(workspaceId: string, page = 1) {
  const from = new Date(Date.now() - 30 * 864e5).toISOString();
  return cb<{ data: CbCall[]; pagination: { has_next_page: boolean } }>(
    `/workspaces/${workspaceId}/calls?from=${from}&page=${page}&per_page=100&include=attribution`,
  );
}

export const ukTime = (iso: string) =>
  new Intl.DateTimeFormat("en-GB", { timeZone: "Europe/London", dateStyle: "medium", timeStyle: "short" }).format(new Date(iso));

How do I keep my copy in sync?

Store calls in your own table keyed by the Call Bullet id, then fetch only what changed since your last sync with updated_since. Calls change after they end: a transcript arrives, a lead status is set or a value is added, so syncing by update time catches all of it.

TypeScript · incremental sync
export async function syncCalls(workspaceId: string, lastSync: string) {
  let page = 1;
  for (;;) {
    const res = await cb<{ data: CbCall[]; pagination: { has_next_page: boolean } }>(
      `/workspaces/${workspaceId}/calls?updated_since=${encodeURIComponent(lastSync)}&page=${page}&per_page=500`,
    );
    for (const c of res.data) await upsertCall(c); // upsert on c.id
    if (!res.pagination.has_next_page) break;
    page++;
  }
  return new Date().toISOString(); // save as next lastSync
}

async function upsertCall(c: CbCall) {
  // insert … on conflict (id) do update
}

For instant updates instead of a schedule, subscribe to the signed call.completed webhook described in the webhooks guide.

How do I play recordings safely?

Call GET /calls/{id}/recording only when has_recording is true. It returns listen_url, a branded listen page valid for seven days, and url, a direct audio link valid for fifteen minutes. Fetch it on demand when someone clicks play, rather than storing links, so recordings never leak through old URLs. This needs the recordings:read permission; transcripts need transcripts:read.

How do I avoid double counting imported history?

Workspaces that moved from another call tracking provider may include imported calls, marked origin: "callrail_import" with a callrail_id. If your system already holds that provider's data, request origin=call_bullet or de-duplicate on callrail_id. The API also returns a warning header so your app can detect it.

What prompt do I paste into my AI app builder?

This prompt builds a complete call log page.

AI build prompt · call log dashboard
Build a Calls page in this app using Call Bullet.
- Docs: https://callbullet.com/developers/api.md. Use only GET /workspaces, GET /workspaces/{id}/calls, GET /calls/{id}/recording and GET /calls/{id}/transcript.
- Store my key (cb_live_…, scopes workspaces:read, calls:read, attribution:read, recordings:read, transcripts:read) as the server secret CALL_BULLET_API_KEY. Server-side calls only.
- Table, newest first: date/time in Europe/London, caller (formatted UK way), status badge (Answered / Missed in red), duration m:ss, source / medium, New caller badge when is_first_time_caller.
- Filters: date range, answered/missed, source. Paginate with page and per_page=100.
- Row click opens a drawer; fetch the recording link only when Play is pressed and has_recording is true.
- Sync hourly into my own table with updated_since, upserting on call id. Request origin=call_bullet to skip imported history.

What limits should I design for?

Plan around the documented limits and the dashboard stays fast.

  • Call lists allow 600 requests a minute per key; other endpoints 120.
  • per_page goes up to 500 for syncs; use 50–100 for screens.
  • A 429 response includes Retry-After; wait that long before retrying.
  • Money fields are in pence; divide by 100 and show £.

See the inbound call tracking API guide for every field and filter.

How do I build useful summaries on top of the log?

Use the same call data to add a few headline numbers above the table.

Most owners want to know how many calls came in today, how many were missed and not yet returned, how many were new callers, and which source brought the most. All four come from the calls you have already fetched: count by answered, is_first_time_caller and source. Show them as four small cards, with the missed-call card linking to a filtered list.

For longer trends, group stored calls by week and source in your own database, rather than calling the API for every chart. Your synced copy keeps charts fast and lets you join calls to your own data, such as which calls became paid jobs.

How do I handle a workspace with no calls yet?

Show a helpful empty state rather than a blank table.

A brand new workspace has numbers but no calls. Explain that calls appear here as soon as someone rings a tracking number, show the numbers from GET /workspaces/{id}/numbers so staff can test by ringing one, and link to the setup checklist in Call Bullet. Once the first call arrives, usually within seconds of a test call, the table fills in and the empty state disappears.

How do I filter and search the log?

Use API filters for date and status, and your own synced table for free-text search.

The calls endpoint accepts from, to and updated_since, plus origin to separate tracked calls from imported history. For everyday searches, such as a caller's number, a customer name or a campaign, query your own synced copy, which can join calls to your contacts and jobs. That keeps searches instant and avoids spending API requests on every keystroke.

Normalise phone numbers to +44 format before saving, so 07700 900123, +447700900123 and 447700900123 all match the same customer. Show the formatted number in the table, but search on the normalised value.

How do I keep call data private?

Call data contains personal information, so keep it behind your app's login and fetch sensitive parts only when needed.

Only staff who need calls should see the Calls page, and recordings should be opened through a fresh link each time rather than stored. Choose the narrowest key scope for each feature: a missed-calls widget needs calls:read, not recordings. Delete synced calls from your own database when your retention policy says so, and explain call tracking and recording in your privacy notice. Call Bullet's call recording and AI policy describes how recordings are handled on its side.

How often should I sync calls?

Every five to fifteen minutes is plenty for most dashboards, with webhooks for anything that must be instant.

A sync every few minutes with updated_since uses only a handful of requests an hour, well inside the limits, and picks up transcripts and lead status changes that arrive after a call ends. If staff need to see a call the moment it finishes, for example on a reception screen, add the call.completed webhook and keep the scheduled sync as a safety net in case a delivery is missed. Store the time of each successful sync, and on failure retry from the last good time so no calls are skipped. Show a small “last updated” time on the page, so staff know how fresh the list is, and add a refresh button that runs the same sync on demand. Because every call is upserted on its ID, running the sync twice never creates duplicate rows. Log each sync's start time, the number of calls changed and any error code returned, so when someone asks why a call is missing you can see exactly when the last successful sync ran and what it fetched. A short daily report of sync health is usually enough.

Key takeaways

  • GET /workspaces/{id}/calls returns caller, time, status, duration and source.
  • Show times in UK time and highlight missed calls.
  • Sync with updated_since and upsert on the call id.
  • Fetch recording links on demand; they expire.

Frequently asked questions

How do I show only missed calls?

Filter answered=false or call_status=missed on the calls you fetch.

Can I match calls to my customers?

Yes. Normalise your contacts' numbers to +44 format and match on from_number.

How far back can I read calls?

Use from and to to pick any range the workspace holds, paging through results.

Do I need webhooks?

No. A scheduled sync with updated_since works; webhooks add instant updates.

Can I show transcripts?

Yes, with transcripts:read, from GET /calls/{id}/transcript when has_transcript is true.

Are outbound calls included?

Outbound calls from phone lines are listed separately at GET /workspaces/{id}/outbound-calls.

See which marketing makes your phone ring

Start with one UK tracking number or a full website pool. Every call is recorded, transcribed and traced back to its source.