Zephoriam

Agentic IVR · voice first · early access

An IVR that can prove what it said.

Zephoriam records every call as an append-only ledger of typed events — every prompt, every caller utterance, every routing decision, every piece of evidence the AI consulted — replayable end to end, so QA, compliance and coaching finally read the same record.

Pre-launch, built by a contact-center consultant who got tired of asking vendors questions they couldn't answer. No customers yet — you would be early, and we would rather say that than fake a logo wall.

Sign up is open. It goes to the product itself, at app.zephoriam.com — a passkey instead of a password, a name for the business, and a number to point at us. It is early, and it looks it: you will be shaping the conversation your callers hear, not picking from a catalogue.

call ledger optometry_appointments v4 · 39 events
  1. call.startedseq 242
    Inbound · from +1 (207) ••• •••• · to +1 (572) ••• •••• · flow optometry_appointments v4
  2. agent.saidseq 245
    “Thanks for calling Northgate Eye Care — this is their automated assistant. This call may be recorded for quality.”
  3. caller.saidseq 249
    “Can I book an appointment?”
  4. intent.classifiedseq 250
    make_appointment · confidence 1.0 · from the declared branches
  5. agent.saidseq 253
    “What day and time works best for you?”
  6. caller.saidseq 254
    “let's see, do you have anything on September 2nd around 9:00 a.m.”
  7. utterance.unhandledseq 255
    At node ask_when, slot preferred_time · “let's see” wasn't in the grammar · the caller's words vaulted verbatim, evidence reference recorded
    Captured for the improvement queue — this is how the platform learns. it didn't guess at the time; it said so, kept the miss as evidence, and asked again
  8. agent.saidseq 258
    “Sorry, I didn't catch a day and time. I can put you through to a member of the team, or you can give me a date and a time…”
  9. caller.saidseq 259
    “September 2nd at 9:00 a.m.”
  10. slot.filledseq 260
    preferred_time = 2026-09-02 09:00 · parser when
  11. appointment.bookedseq 263
    Written to the appointment book — not promised, written
    northgate_eye_care2026-09-02T09:00ref ZPH-7026
  12. agent.saidseq 265
    “Yes, Wednesday September 2 at 9:00 AM is open. I'll hold that for you.”
  13. agent.saidseq 268
    “Can I confirm the number to reach you on, the one ending ••••?”
  14. caller.saidseq 269
    “Yep, that's fine.”
  15. agent.saidseq 273
    “You're booked for Wednesday September 2 at 9:00 AM. Your reference is Z-P-H, 7-0-2-6.”
    The write happened at seq 263; the sentence at seq 273 is only allowed because it did. no completed action, no claim of one
  16. call.endedseq 280
    Outcome: completed · agent_hangup
A real call to our own line, August 28, 2026 — the first appointment the platform ever booked. The caller's number is redacted, nothing else changed. Every line is an event on the ledger and the seq numbers are genuine; the gaps are node and branch mechanics, elided here for space.

Why it exists

Three questions no vendor could answer.

Zephoriam began as a short list of things a contact-center consultant kept asking IVR vendors on discovery calls, and kept getting slides back instead of answers.

  1. 01

    When, where and why do callers drop out?

    Every platform reports containment. Almost none can tell you that a caller heard the wrong branch of the menu, said “representative” twice, got no-matched, and hung up forty seconds in — let alone let you watch it happen again. Drop-out gets inferred from aggregates, weeks later, by whoever is best at spreadsheets.

    Zephoriam's answer: the drop-out is an event, in sequence, sitting next to the prompt that preceded it and the recognition result that caused it. You replay the call instead of reconstructing it.

  2. 02

    Where does the call actually go when the “AI agent” takes it?

    Most third-party voice agents bolt on by blind-transferring to an external phone number. From your side the call simply leaves the building: no events, no transcript, no evidence trail, no way to reconcile what the agent told the caller with what your systems know. It comes back as a line item on someone else's dashboard.

    Zephoriam's answer: the agent is a node inside your flow, not a phone number. Its turns are events on the same ledger as every prompt and every transfer.

  3. 03

    Can you prove what the automation said, and why?

    When a caller insists they were quoted a price, when a supervisor coaches on a call the bot handled first, when someone asks how a disclosure was delivered — “the model generated it” is not an answer, and a recording with no reasoning attached is barely better. QA, compliance and coaching each end up with their own partial export.

    Zephoriam's answer: every agent turn carries the evidence it was permitted to use, by reference. What was said and why it was said live in one record that all three teams read.

How it works

A ledger, an evidence rule, and a gate.

Three architectural commitments. They are the reason the rest of the product behaves the way it does.

one append-only stream per call

The ledger

Typed events — call.started, agent.said, caller.said, intent.classified, branch.taken, appointment.booked, handoff.requested. Nothing is ever overwritten: a correction is a new event, so the record can gain detail but can never quietly change its story. Every screen is a replay of those events, which is why QA, compliance and coaching stop arguing about whose export is right.

the model selects; it never invents

The evidence rule

At an agentic node the model chooses among the branches the flow declares, by index, against a schema — so it cannot route a caller to a destination that doesn't exist. When it states a policy, a price or an account fact, that fact came back from retrieval and is quoted by reference. No retrieved evidence, no claim. The interesting part is what it makes impossible.

the machine drafts; the person decides

The human gate

Sensitive actions — a credit, a refund, an exception — stop for a human approval that is itself a recorded event. So do the platform's own improvement proposals: every request the flow couldn't handle is captured in the caller's own words — that capture runs on live calls today — then mined for patterns and turned into a drafted new branch that a human approves or rejects. The system gets faster. The gate does not move.

The human is the gate. The machine is the draft.

Handoff is an outcome, not a failure

Some intents now finish on their own — the booking in the ledger above did. The ones that don't should end in a person's hands with the work already done: a transfer to a person is a first-class result with a packet attached — the transcript, the classified intent, the evidence consulted, and what the automation already tried. Design for the handoff and it stops being the thing you hide from the report. A contained call that left the caller furious is a failure that the containment number scores as a win.

It learns, but it does not change itself

Every request the flow had no branch for is kept verbatim, as evidence. Those become drafted proposals — a new branch, with the calls that justify it cited by reference. A human approves every change before it reaches a live flow, and the approval is on the ledger like everything else.

The doctrine

The rules this is built under, and what makes each one true.

Every decision on this project is written down, numbered and dated, and the ones below are the rulings that bind how the platform behaves. They are on this page because they are the difference — and because a rule nobody can check is a slogan. So each one names the decision that made it and the thing in the code that enforces it: a database trigger, a guard, a test that goes red. This list is generated from that same code; a line that stopped being enforced could not stay on it. The repository is not public yet, so the citations below are precise on purpose — they name exactly what we can be asked to open.

  1. decision 069, 2026-08-30

    Failures are loud.

    When something breaks, the platform says so and says whose fault it was. A call that ends because our own code fell over is recorded as a platform fault — never as a caller who hung up — and every way a call can stop has a named reason on the ledger.

    The run loop writes the honest ending before it re-raises, and a test proves it: test_an_executor_that_explodes_ends_the_call_as_a_platform_fault swaps a working node for one that throws, and requires the call to end as a platform fault rather than as an agent hangup. A second test holds the vocabulary total — test_the_reason_vocabulary_has_a_value_for_every_way_a_call_can_stop — so no ending can arrive without a name.

    tests/test_honest_endings.py::test_an_executor_that_explodes_ends_the_call_as_a_platform_fault

    tests/test_honest_endings.py::test_the_reason_vocabulary_has_a_value_for_every_way_a_call_can_stop

  2. decision 009, 2026-08-27 decision 125, 2026-08-31 decision 131, 2026-08-31 decision 166, 2026-08-31 decision 181, 2026-08-31 decision 206, 2026-09-01 decision 215, 2026-09-01

    The ledger is append-only. A correction is a new event.

    Nothing on the tape is overwritten and nothing is removed. When a record is wrong the fix is a new event pointing at the old one, so the record can gain detail and can never quietly change its story. And it is not only append-only, it is tamper-evident: once any row on this tape carries a signature, no later row may go unsigned — so a history edited by somebody who got past the database still cannot be made to look untouched.

    Overwriting and deleting are refused by the database itself — triggers on the events table, not application code somebody could route around — and tests prove it: test_update_is_refused_by_the_database issues an update and requires the database to raise, test_delete_is_refused_by_the_database does the same for a delete, and test_correction_is_an_append_not_a_mutation corrects a misheard intent and requires the original row to still read the way it did. A trigger cannot stop somebody who drops the trigger first, so each row can also carry a signature chained to the row before it: test_red_an_altered_payload_is_caught drops the triggers, edits a payload by hand, and requires the chain to name the row that changed. Once that chain is live the tape itself refuses a writer holding no key — test_RED_an_unarmed_writer_CANNOT_append_to_a_live_chain restarts a process without the key against a signed tape and requires the write to be turned away before it reaches the ledger, naming the three settings it is missing. And when this platform's own arming ceremony left a gap, the chain said so — out loud, on the day, about us. The unsigned rows are still there and still unsigned, forgiven by name in a signed declaration every check reports forever: test_a_reanchor_makes_the_incident_verify_and_REPORTS_the_false_start requires the three honesty states to stay three different numbers, and test_cli_the_reanchor_COUNT_is_the_pin_that_catches_a_deletion_behind_it re-signs the whole history with the attacker holding the key, then shows that the count written down away from the machine is what says something is missing.

    tests/test_tape.py::test_update_is_refused_by_the_database

    tests/test_tape.py::test_delete_is_refused_by_the_database

    tests/test_tape.py::test_correction_is_an_append_not_a_mutation

    tests/test_the_tape_mac_chain.py::test_red_an_altered_payload_is_caught

    tests/test_the_tape_mac_chain.py::test_RED_an_unarmed_writer_CANNOT_append_to_a_live_chain

    tests/test_the_tape_mac_chain.py::test_a_reanchor_makes_the_incident_verify_and_REPORTS_the_false_start

    tests/test_the_tape_mac_chain.py::test_cli_the_reanchor_COUNT_is_the_pin_that_catches_a_deletion_behind_it

  3. decision 003, 2026-08-27 decision 123, 2026-08-31

    A human decides what matters, and the decision is a row on the tape.

    The machine drafts; the person decides. Anything outside the published policy — a booking outside the organization's own hours, an outbound call to a real number — stops for a human approval that is itself a recorded event, naming who decided and what they were answering.

    test_no_path_writes_an_appointment_outside_policy_without_a_grant_on_the_tape walks every out-of-policy booking on a finished tape and requires each one to cite a granted approval as its cause — so a write cannot arrive by some other road and be argued about later. test_a_second_decision_on_a_decided_request_loses_at_the_append proves a second decision on an already-decided request loses at the database, not in a branch somebody could delete. And test_the_request_is_written_for_the_promise_and_CITES_it proves the platform asks before it would ring somebody back: the request names the obligation it is about, by sequence number.

    tests/test_the_gate.py::test_no_path_writes_an_appointment_outside_policy_without_a_grant_on_the_tape

    tests/test_the_gate.py::test_a_second_decision_on_a_decided_request_loses_at_the_append

    tests/test_the_promise_asks.py::test_the_request_is_written_for_the_promise_and_CITES_it

  4. decision 087, 2026-08-30 decision 174, 2026-08-31

    No uncited numbers.

    A number this platform shows you names the events it was folded from, and clicking it unspools the call to them. If a number cannot cite its sources it does not ship as a number — a rule we point at our own marketing as hard as at the product.

    test_every_figure_the_console_renders_cites_the_seqs_it_is_a_fold_over opens four operator screens through the real HTTP door and requires every figure on them to link to the sequence numbers it folds over — and the citation to land inside the tape it claims. This page is held to the same rule: the status section below is generated from the code by the command that generated this list, and test_the_shipped_status_section_is_byte_identical_to_the_fold fails if the published page and the platform disagree by one byte.

    tests/test_console.py::test_every_figure_the_console_renders_cites_the_seqs_it_is_a_fold_over

    tests/test_website_status.py::test_the_shipped_status_section_is_byte_identical_to_the_fold

  5. decision 022, 2026-08-27 decision 123, 2026-08-31

    The model picks from a list. It never composes a fact.

    At an agentic node the model returns an index into the branches the flow declares. A destination name, a number off the end of the list, a decimal, nothing at all — every one of those is an illegal answer, and the call takes its declared failure edge instead of guessing. And no sentence may claim a completed action unless a write on the ledger licensed it.

    test_a_router_that_returns_anything_but_a_legal_index_takes_on_failure feeds the router six illegal answers in turn and requires the failure edge and one recorded error every time; on the model lane, test_a_hallucinated_index_is_voided_evented_and_not_retried requires an out-of-range answer to be voided on the tape and not asked again. test_a_flow_that_reaches_the_confirmation_without_a_booking_goes_red rewires a flow so the confirmation is reachable with no booking behind it, and requires both the publish lint and the running call to refuse to say it.

    tests/test_interpreter.py::test_a_router_that_returns_anything_but_a_legal_index_takes_on_failure

    tests/test_model_lane.py::test_a_hallucinated_index_is_voided_evented_and_not_retried

    tests/test_first_completion.py::test_a_flow_that_reaches_the_confirmation_without_a_booking_goes_red

  6. decision 063, 2026-08-29 decision 067, 2026-08-30

    A question may confirm that a record exists. It may never say what is in it.

    Caller ID is a routing hint and never proof of who is calling. So before a caller has been verified the agent may confirm that they have an appointment with the organization — never the day, the time, the provider or the name. The rule is here because a spoofed number should not be able to find out where somebody is going to be.

    The floor is not configurable, and a test proves an organization cannot lower it: test_ani_only_can_never_satisfy_a_content_disclosure takes every gate that discloses content, sets it to the lowest rung, and requires the platform to refuse the configuration by name and to name the decision in the refusal. test_a_disclosure_node_REFUSES_TO_SPEAK_without_a_rung_on_the_tape drives a real call to the disclosure with no verification recorded, and requires the node to refuse to speak.

    tests/test_the_ladder.py::test_ani_only_can_never_satisfy_a_content_disclosure

    tests/test_the_ladder.py::test_a_disclosure_node_REFUSES_TO_SPEAK_without_a_rung_on_the_tape

  7. decision 014, 2026-08-27

    Every miss is kept, in the caller's own words.

    When a caller asks for something the flow has no branch for, the platform does not paraphrase it and does not throw it away. The words are stored verbatim as evidence, the event cites the turn they were said on and the options that were actually on offer, and the caller is offered a person.

    test_an_unhandled_request_lands_citing_the_caller_turn_and_the_caller_words calls the live flow with a request nothing handles, then requires the recorded miss to cite the caller's own turn, to list the branches that were declared at that node, and to hand back the caller's exact bytes from the vault. test_an_unhandled_utterance_cannot_cite_a_turn_that_does_not_exist refuses a miss whose citation does not resolve, and test_an_unsupported_request_hears_the_human_first_offer holds the offer of a person.

    tests/test_unhandled.py::test_an_unhandled_request_lands_citing_the_caller_turn_and_the_caller_words

    tests/test_unhandled.py::test_an_unhandled_utterance_cannot_cite_a_turn_that_does_not_exist

    tests/test_unhandled.py::test_an_unsupported_request_hears_the_human_first_offer

  8. decision 168, 2026-08-31 decision 171, 2026-08-31

    Two people are never handed the same piece of work.

    When several people are working the same list, an item is claimed exactly once. The winner is not picked by a check our own code runs and hopes about: the store refuses the second claim at the write, so the guarantee holds across a second process, a second server and a redeploy. Nobody rings the same person twice about the same thing.

    A partial unique index over the claim's own subject is the referee, and the refusal is the append failing rather than a branch somebody could delete. test_RED_CASE_6_two_claimants_race_one_obligation_and_the_STORE_picks_one starts two real threads at a barrier, lets both of them fold the same unclaimed item, and requires exactly one row to land, the loser to learn it as a typed refusal naming who holds the item, and the item to be gone from the loser's next fold — not greyed out, gone. test_the_partial_unique_index_exists_over_the_SUBJECT_and_is_partial_on_the_KIND then reads the guard's definition back out of the schema, because a fence nobody can find in the database is a fence that can quietly stop being there.

    tests/test_the_claim.py::test_RED_CASE_6_two_claimants_race_one_obligation_and_the_STORE_picks_one

    tests/test_the_claim.py::test_the_partial_unique_index_exists_over_the_SUBJECT_and_is_partial_on_the_KIND

  9. decision 176, 2026-08-31 decision 178, 2026-08-31 decision 789, 2026-09-13

    Chrome pays rent.

    The most valuable thing on a screen gets the most room; everything else takes as little as it can without becoming harder to use. It is a design rule, and it is enforced the only honest way a design rule can be — as a number a test reads.

    test_the_console_centres_in_its_column_and_its_chrome_rows_are_the_same_height reads the operator console's own stylesheet and requires three things: that the console's width follows the founder's ruling -- full width until 789, a centred 1,180 px column since, the customer door keeping the full width where its side pane earns it -- that the two rows of chrome above the work declare the same height as each other, and that the height is smaller than it was before the ruling. The same test requires the Roll's own width cap to be still standing, because a ruling may not widen a surface it did not name.

    tests/test_console.py::test_the_console_centres_in_its_column_and_its_chrome_rows_are_the_same_height

Honest status

Early, and saying so.

Most of this page describes architecture that exists, is under test, and has now taken real calls on our own phone number — and on one of them, booked a real appointment end to end. Here is the line between what is built, what is being built now, and what simply is not true yet.

Built and tested

  • Append-only call ledger with a typed event registry — 102 event kinds today, every one of them carrying a required example, and nothing ever overwritten: a correction is a new event
  • End-to-end replay of a call from its events — every screen is a fold over the ledger, never a second copy of the story
  • Flows are published data, not code — 18 node types to build one from, and a node that declares no timeout edge and no failure edge does not validate, so a call cannot fall off the end of the flow into silence
  • Declared-branch routing — the model picks an index out of the branches the flow declares, against a schema, so it cannot route a caller to a destination that does not exist
  • A claim guard at publish time: 17 sentence shapes that assert a completed action are refused unless a write licenses them, and the only two node types that may say a thing was done (confirm_booking, confirm_cancellation) refuse to speak without the write on the ledger first
  • Human approval gates as first-class recorded events — approval.denied, approval.granted, approval.requested
  • Live voice on a real phone number, over Twilio — every webhook authenticated before it can append anything, by the carrier's own request signature, and the server refuses to start with no way to check one
  • The first flow live on that number, 66 nodes: book, change or cancel an appointment. All three complete — real writes to our own appointment book, each one licensing the sentence that claims it, and a reschedule inside the organization's own notice window goes to a person because the organization said so
  • Our own appointment book, built and used: the first real booking was written on a live call, reference ZPH-7026, with the write on the ledger before the agent was allowed to say “you're booked”
  • Unhandled requests captured verbatim, the caller's own words vaulted as evidence — and keypad answers accepted anywhere the flow asks a question
  • The Ears: a second recognizer on a media stream, running beside the phone's own. It exists for the one prompt the phone is deaf during — the compliance greeting, which by design cannot be interrupted — and it writes 8 typed events of its own. A node may answer from it only where the flow says so, per node, as data — and when a reading is left over from a turn the phone already took, the refusal to use it is an event too
  • Customer organizations, plural, on one ledger: 4 flows declared in this repository and 1 appointment book — the flows found by a registry that walks the flow package rather than by a list somebody maintains, and 2 of them publishable from this platform's own CLI. Every event is scoped to the organization it belongs to, and the number a caller dialed — not a config flag — selects whose greeting, whose flow and whose book answer
  • The screens: the Roll (one call unspooled, sequence numbers in the margin) and an operator console of 19 GET-only screens behind passkey sign-in — a request with no passkey session reaches no screen and no write but the 3 ceremonies that sign a person in, enroll a passkey and recover one. It runs on one small instance, and exactly 9 of its routes can write to the ledger — each one named in the route table with the argument for it written beside it, every decision among them recorded under the signed-in person who made it, and each refusing rather than writing something it cannot attribute

Being built now

  • Handoff to a real destination — the packet is assembled from the tape on every handoff today, and a caller who confirms a number is already recorded as a completed callback; the live transfer leg is the next thing wired
  • A model lane for routing, running dark beside the deterministic path: it is off unless somebody sets a flag, it judges calls it cannot touch, and it does not go live until it beats the deterministic path on 10 real judged calls and scores the same corpus the same way 2 times running. It now scores reproducibly; it does not yet beat the deterministic path

Not true yet

  • No customers. Every call on this page was dialed to our own line by the person building the thing — that is a test, not a customer, and we are not going to describe it as traction. No case studies, no logos, no testimonials, no usage statistics — and none invented in the meantime
  • No compliance certifications. The architecture is built for auditability; that is not the same as having been audited, and we won't blur the two
  • Nothing is mined into proposed branches yet. The capture is built and the misses have a reader; turning a corpus of them into drafted proposals is designed, not written — there is no proposal on the ledger because there is no event kind for one
  • One channel carries traffic: voice. The TEXT channel's transport is BUILT AND DARK — the event kinds are registered, an inbound message and a STOP would land on this same append-only ledger, and every outbound message is REFUSED by one send primitive with a typed reason and a row of its own until somebody deliberately arms it. Nothing in this repository can send one: there is no client here that dials a messaging vendor, and 'no transport' is a standing refusal rather than a gap. Chat and email are designed and have no kinds at all
  • No single sign-on yet: every sign-in is a passkey, on the door and on the console, and every one of the 9 things the console may write is attributed to the person who pressed it

If this is the IVR you have been describing to vendors, talk to the person building it.

Early access means small numbers of design partners, direct access to the build, and a real say in what the first flows look like.

Both go to the founder — a contact-center consultant, not a sales team.