The worker you already run for inbound calls (Run a LiveKit Agent on TeleQuick Transport) serves outbound calls too, unchanged. The engine dials the number on your trunk, and when the callee answers it dispatches the call to your registered worker exactly like an inbound call. So there are only two moving parts:
  1. The worker — already running, already registered. No change.
  2. The trigger — a dial-out request. This page.
LiveKit agents dial out with lkapi.sip.create_sip_participant(...). The transport plugin ships the same gesture — TeleQuickAPI — so outbound code written against LiveKit’s API moves across with a changed import. Underneath it is one call to the administration API, not a LiveKit server.

Credentials

Dial-out is a control-plane action, so it uses your org-scoped mpk_… API key — not the media app key/secret the worker authenticates its transport with.
The key acts as its creator, confined to one organization. Mint a narrow one for the dialer rather than reusing an administrative key, and never ship it to a browser.

Dial one number

agent_id is the TeleQuick agent (console id) that answers. For an external agent that is the VENDOR_BRIDGE config whose vendor_room is the handle your worker registered under. room_name / roomName is accepted as a LiveKit-idiom alias for the same field.

What the worker receives

Your worker gets a job_assign when the callee answers — the same event an inbound call produces. One agent script therefore serves both directions; branch on the numbers if your outbound flow differs.
The two plugins surface SIP attributes differently today — TypeScript puts them on ctx.room.attributes, Python on the remote participant. See What your agent sees for the exact table.

Dial a list — a paced campaign

Hand the engine the whole list and it paces the dial-out itself: calls per second and maximum concurrency are enforced server-side by the campaign manager. You do not write a limiter.

Campaign limits

A campaign_id is generated for you when you do not pass one. list_campaigns returns the persisted campaign rows — campaign_id, name, status, total_numbers, loaded_numbers, created_at — newest first. abort_campaign stops further dialling; calls already in progress are not torn down.

A runnable example

The starter repo ships this as src/outbound.py:
It reads TELEQUICK_API_KEY, TELEQUICK_ORG_ID, CC_TRUNK, and CC_AGENT_ID from the environment. The receiving side is the same telequick_worker.py that serves inbound.

Beyond dial-out

TeleQuickAPI deliberately wraps only the dial-out calls an agent process needs, in the shape LiveKit code already uses. For the rest of the control plane — trunks, numbers, agents, CDR, recordings — use the typed Admin API SDK, which reaches the same BFF with the same key.

Run a LiveKit Agent on the Transport

The worker that answers these calls.

Outbound Sales Agent

The same flow for a native agent, with voicemail handling and warm transfer.

Recordings API

Fetch the audio for every call the campaign placed.

Embed an Agent Widget

The same worker, reached from a browser instead of the PSTN.