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.

  1. Copy the agent API key from Agent → Website/Deploy → API access.
  2. Build the tag with the widget generator, or write it by hand.
  3. Paste it immediately before </body>.
  4. Load the page, press the launcher, and talk to it. If nothing appears, open the console - a missing data-key is logged there.
index.html
<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>
The part that catches people out

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.

  1. In the agent, open the calendar integration and connect Calendly - it returns an authorisation URL you approve in Calendly.
  2. Pick the event type the agent should book into. Your event types are read from the live account.
  3. Add the booking instruction to the prompt: what to ask for, and what to do when the preferred slot is gone.
  4. Call the agent and book a real appointment. Then check it in Calendly.
prompt fragment
## 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.
Availability is enforced by Calendly, not by the agent. Write the prompt so it offers alternatives instead of promising a specific time.
The part that catches people out

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.

  1. Point the agent webhook at the webhook tester and press Test. Read the real payload - the field names are not published anywhere.
  2. Write a consumer that answers 200 first and stores the raw body before doing anything else.
  3. Normalise the caller number to E.164. Without this, every call creates a duplicate contact.
  4. Create the CRM record only when the summary says follow-up is required. Every resolved call in the pipeline is noise.
  5. Point the webhook at your endpoint and have a real conversation to confirm it end to end.
the shape that survives
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'
  })
}
Full per-CRM versions: HubSpot and Pipedrive under Integrations.
The part that catches people out

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.

  1. Create an incoming webhook in Slack for the channel that should receive it.
  2. Write a transform: summary title, outcome, sentiment and the key points. Not the transcript.
  3. Colour the attachment by sentiment so a bad call is findable by scroll speed.
  4. Mention the channel only on a negative outcome. A mention on every call gets the integration muted within a week.
The part that catches people out

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.

  1. Start out of hours. Nobody is answering those calls, so the agent cannot make anything worse, and you get real transcripts within a day.
  2. Point your existing number at sip.bitpull.ai:5060 with an out-of-hours rule in your PBX, or buy a number in the dashboard and publish it as a second line.
  3. 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.
  4. Enable the email summary so somebody reads every conversation for the first week.
  5. Only then widen the rule - overflow after N seconds, then daytime.
The part that catches people out

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.

  1. 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.
  2. Build the endpoint. Return three fields the agent may say out loud - never the whole order object.
  3. Set a tight timeout. The caller hears every millisecond of it.
  4. Cache. A shop API call inside a live conversation is the segment you control, and the one that most often ruins a call.
tool declaration
{
  "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
}
Full Shopify and WooCommerce versions, including the endpoints, are under Integrations.
The part that catches people out

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.

  1. Set the redirect target on the phone number in the dashboard.
  2. Write the escalation rule into the prompt: which situations transfer, and what the agent says first.
  3. Decide what happens when nobody picks up. A transfer into an unanswered phone is worse than no transfer.
  4. Use the webhook to deliver the summary afterwards - the transfer carries the call, not the context.
prompt fragment
## 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 part that catches people out

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.

  1. Create the session on your server so the API key never reaches the browser.
  2. Apply your own gate first: signed-in user, rate limit, quota. The hosted widget cannot do this; here you can.
  3. Return only sessionId, wsUrl and token to the client.
  4. Join the room, attach the audio, publish the microphone, and render whatever you want around it.
  5. End the session server-side on pagehide. Sessions that are never ended are the usual cause of an unexplained minute count.
server + browser
// 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 }))
)
The part that catches people out

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.