Skip to main content
Every PAL you edit in the PAL Builder has two rows behind a single pal_id: a live row that powers conversations, deployments, and public reads, and a draft row that the Builder writes into as you edit. Publishing copies the draft over the live row. For most integrations you never need to think about this — you call Create PAL or Patch PAL with the same pal_id and things work. This page exists because the API endpoints that read and write PALs expose the draft/live split when a Builder session is open, and you may want to opt into (or out of) that behavior explicitly.

The model

A PAL created purely through the API — never opened in the PAL Builder — has no draft. All of the behavior on this page applies only once a Builder session has produced a draft row.
  • Live PAL — the version of the PAL that runs when a conversation starts, when a deployment answers a call, or when a Google Meet invite arrives. Public reads (GET /v2/pals/{pal_id}) return this row by default. This is the only row that is exposed to end users.
  • Builder draft — a hidden copy of the PAL that the PAL Builder writes into while you edit. It shares its pal_id alias with live from the caller’s perspective, but has its own internal row so the Builder can preview changes (including “Test” conversations) before publishing.
  • Publish — copies the draft’s content over the live row. Until you publish, live traffic keeps using the previously-published version.

How PATCH picks a row

PATCH /v2/pals/{pal_id} routes to the draft by default when a PAL Builder draft exists for that PAL:
  • If a draft exists, the patch lands on the draft. Live conversations continue to use the previously-published version until you publish.
  • If no draft exists (the PAL has never been opened in the Builder), the patch lands on live, exactly as before.
  • Pass ?target=live to bypass the draft. Tavus writes to live and resets the draft to match live, so live becomes the single source of truth again. Any unpublished draft edits are discarded.
This default keeps API and UI edits consistent. The Builder reads the draft row, so a PATCH that skipped the draft would produce an editor showing the pre-patch state — the classic “my edit didn’t show up” symptom. Routing to the draft by default means an API caller and a human editor never disagree about what “the current PAL” is. Every PATCH response echoes the routing back so a client can log or branch on it:
  • pal_id is always the id the caller passed in the URL — safe to persist on the client.
  • edit_target is draft or live.
  • draft_pal_id is the underlying draft row. You do not usually need it — patching or publishing the live id automatically routes to the draft — but it’s returned for tooling that wants to address the draft directly.
  • publish_url is returned for draft-routed edits so the next API action is explicit.

Google Meet / layers.conferencing is live-owned

layers.conferencing (see Google Meet) is deployment config. It provisions the PAL’s @tavusinvite.com email address, and calendar invites keep arriving while a Builder draft is open. Because of that, conferencing is always owned by the live PAL:
  • A PATCH that touches layers.conferencing routes to live even without ?target=live and syncs the conferencing layer into the open draft. Draft content in other fields is preserved.
  • ?target=draft with a conferencing op is rejected with 400.
  • A single request cannot mix conferencing ops with edits to other fields. Split the request in two if you need to change both.
  • Publishing a stale draft (POST /v2/pals/{pal_id}/publish) — one that predates a conferencing change made against live — preserves the live conferencing config. Publishing never wipes deployed Google Meet integrations.

How GET picks a row

GET /v2/pals/{pal_id} returns the live PAL by default, even when a draft exists. This is the public API contract customers depend on, and it is unchanged.
  • Add ?source=draft to read the draft body without publishing it. The response pal_id still matches the id you queried, and the response adds is_draft_view: true, has_unpublished_changes, live_pal_id, draft_pal_id, published_view_url, publish_url, and a routing_message explaining that conversations still use the live version.
  • If you queried a draft id directly (rare — you usually address the shared live id), add ?source=live to read the live parent’s body instead of the draft.
  • On any draft view, layers.conferencing is still the live value, so a draft read never surfaces a stale conferencing layer.

When to use each mode

You want an API PATCH to behave the way the Builder does — edit safely, publish later

Use the default (no target). Your edits land on the draft, the Builder shows them, and live traffic is unaffected until you publish (POST /v2/pals/{pal_id}/publish).
Pass ?target=live. Live is updated and the retained draft is reset to match, so nothing about the PAL diverges from what your API call just wrote. Any unpublished draft edits (including ones made by a teammate in PAL Maker) are discarded, but the draft row and its id remain available for future Builder edits.
PATCH layers.conferencing on its own and either omit target or pass ?target=live. It is always routed to live and synced into the draft. Explicit ?target=draft returns 400. Do not combine it with other field edits in the same request.
GET /v2/pals/{pal_id}?source=draft. Response body is the draft; is_draft_view is true. Live conversations are unchanged.
POST /v2/pals/{pal_id}/publish. The draft is copied onto the live row, and the response returns the published PAL with status: "success". If the draft predates a conferencing change made against live, the live conferencing config is preserved.

Migration notes

If you were patching PALs against the API before the PAL Builder draft was introduced, or your PAL has never been opened in the Builder, nothing changes: your patches still apply to live. If your account uses PAL Maker and you patch the same PAL over the API, the default write target has changed — your edits now stack on top of any open Builder draft instead of jumping past it. To restore the previous behavior for a single call, pass ?target=live.