GeniffyDocs
Changelog Log In Get a key

Bolna

Give a Bolna voice agent a memory of each caller. Before Bolna answers an incoming call, it can ask your server about the caller; when you place a call yourself, you send what to know with it; and when a call ends, Bolna sends you its transcript. Answer the first two with what matters about the caller, and save the third. The next call starts knowing what was said in this one.

Install

Terminal
pip install fastapi uvicorn httpx geniffy

Set GENIFFY_API_KEY from API keys in the Geniffy app, BOLNA_API_KEY and BOLNA_AGENT_ID from Bolna, and BOLNA_TOKEN to a long random secret of your own. Bolna sends it with every request, so your server can tell Bolna's requests from anyone else's.

Your server

server.py
import hmac
import os

import httpx
from fastapi import FastAPI, HTTPException, Request
from geniffy import AsyncGeniffy

geniffy = AsyncGeniffy()                          # reads GENIFFY_API_KEY
bolna = httpx.AsyncClient(base_url="https://api.bolna.ai",
                          headers={"Authorization": f"Bearer {os.environ['BOLNA_API_KEY']}"})
TOKEN = os.environ["BOLNA_TOKEN"]
app = FastAPI()


def space_for(number: str | None) -> str:
    """The caller, by the digits of their number. If you know callers as customers, use your customer id, and
    the same memory serves their calls, chats and emails."""
    digits = "".join(ch for ch in number or "" if ch.isdigit())
    if not digits:                                # never fall back to one shared memory for unknown callers
        raise HTTPException(400, "This call has no caller number.")
    return f"user_{digits}"


def check(request: Request) -> None:
    """Bolna sends your token as a bearer token."""
    given = request.headers.get("Authorization", "").removeprefix("Bearer ")
    if not hmac.compare_digest(given.encode(), TOKEN.encode()):
        raise HTTPException(401)


async def known_about(number: str | None) -> str:
    brief = await geniffy.space(space_for(number)).brief(limit=12)
    return "\n".join(f"- {m['text']}" for m in brief["memories"]) or (
        "Nothing is known about this caller yet. If they mention an earlier call, say so rather than guessing.")


@app.get("/bolna/caller")
async def caller(request: Request, contact_number: str = ""):
    """Bolna asks this before it answers an incoming call; what it returns fills {{memory}} in the prompt."""
    check(request)
    return {"memory": await known_about(contact_number)}


async def place_call(number: str) -> str:
    """A call out that starts knowing the person it calls."""
    r = await bolna.post("/call", json={"agent_id": os.environ["BOLNA_AGENT_ID"], "recipient_phone_number": number,
                                        "user_data": {"memory": await known_about(number)}})
    r.raise_for_status()
    return r.json()["execution_id"]


@app.post("/bolna/calls")
async def calls(request: Request):
    """Each update on a call. The completed one carries the transcript; Bolna can send it more than once, so
    it is saved under the call's own id, and a call sent again is the same source."""
    check(request)
    call = await request.json()
    if call.get("status") != "completed":
        return {}
    turns: list[dict] = []
    for line in (call.get("transcript") or "").splitlines():
        role, colon, said = line.partition(":")
        if colon and role.strip() in ("assistant", "user"):
            turns.append({"role": role.strip(), "content": said.strip()})
        elif turns and line.strip():              # a line that carries on what was being said
            turns[-1]["content"] += " " + line.strip()
    turns = [t for t in turns if t["content"]]
    if turns:                                     # user_number: the caller, or the person called
        await geniffy.space(space_for(call.get("user_number"))).memories.add(
            messages=turns, title="Call", external_id=f"bolna-{call['id']}")
    return {}

Run it with uvicorn server:app, somewhere Bolna can reach over HTTPS, and call place_call(number) from your own code to ring someone.

Bolna waits at most three seconds for an answer about a caller, so the agent is given brief(): the caller's most load-bearing memories, newest first, read without a model call. A call is saved once it is completed, under its own id, so when Bolna sends the same call again, nothing is learned twice.

In Bolna

In your agent's prompt, put the memory where you want it:

You are a friendly phone assistant. Keep answers short.

<memory>
{{memory}}
</memory>
  1. On the agent's Inbound tab, match callers through your own API: the endpoint is https://your-server/bolna/caller, and the auth token is your BOLNA_TOKEN.
  2. On the agent's Extractions tab, set the Webhook URL to https://your-server/bolna/calls. Turn on Add headers and add Authorization with the value Bearer followed by your BOLNA_TOKEN.

When nothing is known about a caller, the memory says so in one sentence, so the agent says it doesn't know instead of guessing. Each call is added as one conversation titled Call. Geniffy keeps who said what, so what the caller said becomes a fact about them, and what your agent said stays the agent's.

Last updated October 6, 2026