Examples
Eight things worth building
Complete recipes rather than snippets: what you need first, the steps in order, the code that does the work, and the part that will catch you out.
01 · Put an agent on a website
Voice and text chat on any page, in about two minutes.
You need: An agent on bitpull.ai and its API key. HTTPS on the site - browsers block the microphone without it.
- Copy the agent API key from Agent → Website/Deploy → API access.
- Build the tag with the widget generator, or write it by hand.
- Paste it immediately before
</body>. - Load the page, press the launcher, and talk to it. If nothing appears, open the console - a missing
data-keyis logged there.
<script src="https://bitpull.ai/widget/template.js"
data-key="YOUR_AGENT_API_KEY"
data-lang="en"
data-modes="both"
data-color="#2962ff"
data-teasers="Opening hours?|Book an appointment"
defer></script>On a LAN IP during development the voice mode silently will not work - only localhost and real HTTPS origins count as secure contexts for microphone access.
02 · Create an appointment from a voice conversation
The caller asks for a slot and the booking is in the calendar before they hang up.
You need: A Calendly account with an active event type. This is the one calendar connector that needs no code.
- In the agent, open the calendar integration and connect Calendly - it returns an authorisation URL you approve in Calendly.
- Pick the event type the agent should book into. Your event types are read from the live account.
- Add the booking instruction to the prompt: what to ask for, and what to do when the preferred slot is gone.
- Call the agent and book a real appointment. Then check it in Calendly.
## Appointments
- When the caller wants an appointment, ask for their name and a preferred day.
- Offer a slot that is actually free. If it has just been taken, apologise
briefly and offer the next two alternatives.
- Repeat the confirmed date and time back to the caller before ending.
- If the caller wants to move or cancel an existing appointment, take their
name and number and say a colleague will call back - you cannot change
existing bookings.The Calendly OAuth grant expires, and the connector reports when. Nothing shouts about it; a silently unbookable week is the failure mode. Put a reminder against the expiry date.
03 · Send leads to a CRM
Every conversation worth following up becomes a record where sales already works.
You need: The outbound webhook enabled on your account, and somewhere to run about sixty lines of code.
- Point the agent webhook at the webhook tester and press Test. Read the real payload - the field names are not published anywhere.
- Write a consumer that answers
200first and stores the raw body before doing anything else. - Normalise the caller number to E.164. Without this, every call creates a duplicate contact.
- Create the CRM record only when the summary says follow-up is required. Every resolved call in the pipeline is noise.
- Point the webhook at your endpoint and have a real conversation to confirm it end to end.
export default async function handler(req, res) {
const id = await store.insertRaw({ at: new Date().toISOString(), body: req.body })
res.status(200).json({ ok: true }) // acknowledge before any third party
await queue.publish('conversation.received', { id })
}
// In the worker, where a failure is retryable:
async function onConversation({ id }) {
const body = await store.getRaw(id)
const summary = body.summary ?? body.session?.summary ?? {}
if (!summary.followUp?.required) return // resolved calls are not leads
const phone = e164(body.sipFrom ?? body.from)
const contact = await crm.findOrCreate({ phone })
await crm.addNote(contact.id, renderTranscript(body.messages ?? []))
await crm.createTask(contact.id, summary.title, {
priority: summary.sentiment === 'NEGATIVE' ? 'HIGH' : 'MEDIUM'
})
}No retry is documented on the sending side. If your endpoint is down, the conversation is gone. Persisting the raw body as the first statement in the handler is what turns that into a replayable problem.
04 · Trigger a notification after a call
The team sees what the agent handled without opening anything.
You need: A Slack incoming webhook URL, and the outbound webhook enabled.
- Create an incoming webhook in Slack for the channel that should receive it.
- Write a transform: summary title, outcome, sentiment and the key points. Not the transcript.
- Colour the attachment by sentiment so a bad call is findable by scroll speed.
- Mention the channel only on a negative outcome. A mention on every call gets the integration muted within a week.
A Slack channel has no access control beyond membership, and a transcript is whatever a caller happened to say. Post the summary and link to the detail; keep the transcript somewhere with permissions.
05 · Build an AI receptionist
Calls that nobody answers today get answered.
You need: A phone number - either bought in the dashboard or your existing one, redirected.
- Start out of hours. Nobody is answering those calls, so the agent cannot make anything worse, and you get real transcripts within a day.
- Point your existing number at
sip.bitpull.ai:5060with an out-of-hours rule in your PBX, or buy a number in the dashboard and publish it as a second line. - Write the prompt for the ear: short answers, an AI disclosure in the greeting, a callback path, and explicit limits. Run it through the reviewer.
- Enable the email summary so somebody reads every conversation for the first week.
- Only then widen the rule - overflow after N seconds, then daytime.
A bought number can sit in regulatory verification for days. If the launch date matters, redirect a number you already own instead - that path needs no verification at all.
06 · Answer from live shop data
"Where is my order" answered from the shop, not from a FAQ page.
You need: A tool declaration on the agent, an endpoint of your own, and the tool wiring set up with the bitpull team.
- Declare the tool: a precise name, a description that says when to use it, and a JSON Schema with as few required fields as possible.
- Build the endpoint. Return three fields the agent may say out loud - never the whole order object.
- Set a tight timeout. The caller hears every millisecond of it.
- Cache. A shop API call inside a live conversation is the segment you control, and the one that most often ruins a call.
{
"name": "order_status",
"description": "Look up the fulfilment status and tracking link of an order. Use when the caller asks where their order is, whether it shipped, or when it will arrive.",
"parameters": {
"type": "object",
"properties": { "order_number": { "type": "string" } },
"required": ["order_number"],
"additionalProperties": false
},
"responseTimeoutMs": 3000,
"enabled": true
}The declaration has no target URL in it - connecting it to your endpoint is set up with the bitpull team. Plan for that if you were expecting a self-serve rollout.
07 · Route a conversation to a human
The caller reaches a person when the agent should not be handling it.
You need: A redirect target on the number - an E.164 number or a SIP URI - and a rule in the prompt.
- Set the redirect target on the phone number in the dashboard.
- Write the escalation rule into the prompt: which situations transfer, and what the agent says first.
- Decide what happens when nobody picks up. A transfer into an unanswered phone is worse than no transfer.
- Use the webhook to deliver the summary afterwards - the transfer carries the call, not the context.
## Handing over
- Transfer to a colleague when the caller is angry, describes an emergency,
asks explicitly for a person, or asks about an existing complaint.
- Before transferring, say: "I am putting you through to a colleague now."
- If the transfer does not connect, take the caller's name and number and say
they will be called back today.The person who picks up gets a call, not a transcript. There is no context hand-off - the summary reaches them afterwards through the webhook. Design the greeting on the human side around that gap.
08 · Put an agent inside your own application
A conversational agent in a product you control, with your interface and your rules about who may start one.
You need: A server that can hold the API key, and livekit-client in the browser.
- Create the session on your server so the API key never reaches the browser.
- Apply your own gate first: signed-in user, rate limit, quota. The hosted widget cannot do this; here you can.
- Return only
sessionId,wsUrlandtokento the client. - Join the room, attach the audio, publish the microphone, and render whatever you want around it.
- End the session server-side on
pagehide. Sessions that are never ended are the usual cause of an unexplained minute count.
// server - POST /api/agent/session
const upstream = await fetch('https://api.bitpull.ai/api/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.BITPULL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
language: user.locale,
interactionMode: 'VOICE',
// Context you control. Never interpolate anything the user typed.
systemPrompt: buildPrompt({ plan: user.plan, name: user.firstName })
})
})
const { sessionId, wsUrl, token } = await upstream.json()
// browser
const room = new Room()
room.on(RoomEvent.TrackSubscribed, (t) => t.attach(audioEl))
await room.connect(wsUrl, token)
await room.localParticipant.setMicrophoneEnabled(true)
window.addEventListener('pagehide', () =>
navigator.sendBeacon('/api/agent/end', JSON.stringify({ sessionId }))
)A per-session systemPrompt is a prompt-injection surface. Build it from values your server owns - plan, name, account state - and never from anything the user typed or a query parameter carried.