Keep Bird activation outside the PHP-FPM/nginx startup path so Bird configuration failures cannot make the core API unavailable. Retain guarded explicit activation and regression coverage.
13 KiB
Bird Module
This module provides Bird API integration for voice calls and number management.
Control Plane integration
The Bird module is the credential and public-webhook authority for Pleno Control Plane. Control Plane must never receive the Bird access key. The integration is disabled by default and has three independently protected surfaces:
GET /bird/healthis an end-user-permission-protected, read-only replacement for the outbound-call connection test./bird/control-plane/v1/*accepts only the configuredcontrol_plane_tokenbearer token and exposes explicitly listed read operations. It is not a generic Bird proxy.POST /bird/webhooks/notificationsaccepts only Bird notifications signed against the exact configured public HTTPS URL. It checks the replay window and durably deduplicates both Bird request ID and signature before returning success.POST /bird/flows/evaluateaccepts a timestamp-bound HMAC inx-pleno-flow-timestampandx-pleno-flow-signature. It returns only deterministictag,assign,snooze, orclosedecisions from a valid versioned policy.
Required configuration is:
workspaceId: canonical workspace identifier. While migrating, an empty value falls back to legacyworkplaceId.allowed_channel_ids_json: canonical JSON array of explicitly approved channel IDs. Its empty default falls back to legacychannelId; malformed or non-empty invalid configuration fails closed.control_plane_enabled=falseand secretcontrol_plane_token.- secret
webhook_signing_key, exactwebhook_public_url, andwebhook_replay_window_seconds=300. flow_enabled=false, secretflow_shared_secret, and a validflow_policy_jsondocument.operations_actions_enabled=falsefor confirmed typed voice actions.outbound_messages_enabled=false,participantId, and an immutabletemplate_policy_jsonallowlist for confirmed conversation replies.
The gateway intentionally does not expose conversation creation, number
deletion, physical gate actions, arbitrary recipients, attachments, or a
generic proxy. Outbound references are durably reserved before a provider
request; an ambiguous outcome must be inspected through
/bird/control-plane/v1/messages/by-reference and is never blindly retried.
Credentials and write switches can only be changed by the guarded CLI
activation command, never by a web request.
Schema deployment and preflight
The durable webhook and outbound-action ledgers use checked-in schema version
1. Schema changes are never run from a web request, worker, or core API
startup. Use these commands for deployment preflight and recovery:
php scripts/bird-control-plane-schema.php check
php scripts/bird-control-plane-schema.php apply --yes
php scripts/bird-control-plane-schema.php check
apply is CLI-only and requires the explicit --yes guard. The status endpoint
publishes read-only schema: {ready, version, expectedVersion, missing} state.
Ledger-dependent reads, webhook ingestion, messages, and operational actions
fail closed with HTTP 503 and code bird_schema_not_ready until the preflight
is ready. Explicit activation applies and verifies the schema before enabling
any Bird write switch.
Fail-closed production activation
Bird activation is deliberately separate from PHP-FPM and nginx startup, so a Bird provider or configuration failure cannot make the core API unavailable. Run the guarded activation explicitly in the deployed API container:
php /var/www/html/scripts/bird-control-plane-auto-activate.php
The command first transactionally disables bootstrap readiness and all three
write switches. It then canonicalizes the existing legacy workspace/channel
configuration, applies and checks schema version 1, validates configured
channels and conversations, resolves the access-key participant, pins the
public webhook URL to
https://api.truckwash.io:4433/bird/webhooks/notifications, and reconciles and
verifies webhooks while writes remain dark. Only then does one transaction
enable bootstrap readiness, the Control Plane, outbound messaging, and
operational-action switches, followed by final readiness checks.
Any failure returns a non-zero exit code and re-disables the Bird capabilities
without stopping or restarting the core API.
It preserves existing valid credentials; otherwise it generates distinct
48-byte random Control Plane and webhook secrets inside the container. The
webhook key remains backend-only.
The Control Plane token is sealed with the committed RSA-3072 public key using
RSA-OAEP-SHA256 with MGF1-SHA256. OpenSSL receives plaintext only over child
stdin, never argv. Only the ciphertext, RSA-OAEP-256 algorithm identifier,
SPKI SHA-256 fingerprint
6dc63c6ffe33b8de0b1396d7f529f56aea0a685ef98168016161cf721ddc8c21,
monotonic token version, and UTC update timestamp are bootstrap-visible.
Existing ciphertext is retained only when its internal token hash and all
metadata remain valid, so normal deployments do not rotate a working token.
Webhook subscription reconciliation runs before service readiness. The
reconciler first requires Bird's available-webhooks response to advertise the
conversations service, both conversation.created and
conversation.updated, and channelId filtering for each. It prefers the
documented organization/workspace list after deriving a single UUID-like
organization ID from workspace-consistent channel/conversation metadata.
When no organization ID is discoverable, the read-only preflight may probe the
existing workspace-scoped subscription list endpoint. That fallback is
accepted only when it returns an explicit results, items, or data
collection with bounded pagination. Any unsupported response, ambiguous page,
or provider error becomes organization_id_required before a POST or PATCH.
The reconciler creates one exact subscription for each event and allowlisted
channel, or patches only an existing subscription with the exact Pleno URL,
event, and sole channel filter. It never deletes or mutates unrelated
subscriptions.
Any schema, provider, encryption, subscription, or final readiness failure transactionally restores bootstrap readiness and all three write switches to false, then exits container startup before PHP-FPM/nginx starts. Flow and template automation remain disabled unless their existing versioned policies are explicit, non-empty, and structurally valid; Flow also requires an existing strong shared secret.
Local Control Plane credential bootstrap
The unauthenticated GET /bird/control-plane/v1/bootstrap response is
Cache-Control: no-store and contains only the fixed validated ciphertext
envelope. Unavailable state always returns the same 404 Not found response.
The endpoint never returns plaintext, hashes, provider credentials, or the
backend-only webhook key.
On the Control Plane host, run:
scripts/bird-control-plane-bootstrap-local.sh
The script is pinned to https://api.truckwash.io:4433 and accepts no URL
override. It validates the exact algorithm, public-key fingerprint, fixed
ciphertext shape, version, and timestamp, decrypts using the local 0600
private key, and writes only a temporary 0600 candidate. It immediately proves
the bearer against the authenticated status endpoint and atomically retains it
as /home/jeppe/.openclaw/credentials/bird.gateway-token only after success.
Endpoints
Voice calls
-
POST /bird/voice/calls- Permission:
modules_bird_voice_calls_create - Required:
workspaceId,channelId - Body: passthrough to Bird call create API (except
workspaceIdandchannelId).
- Permission:
-
GET /bird/voice/calls- Permission:
modules_bird_voice_calls_list - Required query:
workspaceId,channelId - Query params are passed through to Bird.
- Permission:
-
GET /bird/voice/calls/{id}- Permission:
modules_bird_voice_calls_get - Required query:
workspaceId,channelId
- Permission:
-
POST /bird/voice/calls/{id}/hangup- Permission:
modules_bird_voice_calls_hangup - Required:
workspaceId,channelId
- Permission:
-
POST /bird/voice/calls/test-outbound- Permission:
modules_bird_voice_calls_test_outbound - Required:
workspaceId,channelId - Places an outbound call to the fixed test number
+45 42 33 11 28(+4542331128), polls call state, and sends hangup when state isacceptedorongoing. - Optional tuning params:
pollIntervalSeconds(default2)maxPollSeconds(default30)hangupCause
- Permission:
-
POST /bird/voice/calls/webhook/inbound- Permission:
modules_bird_voice_call_webhooks_trigger - Stateful inbound IVR webhook for phone-controlled department gates.
- On the initial request, the webhook accepts the live inbound call through Bird before returning the first gather command.
- Supports both request formats:
- Native-flow mode: top-level
{ callId, channelId, workspaceId }for the initial fetch, then{ callId, channelId, workspaceId, keys }for selections. - Compatibility mode:
{ payload, request, waitConditions }, which returns a raw BirdcallCommandgather envelope.
- Native-flow mode: top-level
- Uses
department_gatesrows withconfig.type=PHONE_CALLas the source of truth for which departments and entrance/exit gates are offered. - Department menu order follows
departments.order_priority. - Supports multi-digit department selections such as
10#. - Always prompts for both department selection and gate selection, even when only one valid option exists.
- Uses compact gate numbering:
- both gates available:
1=entrance,2=exit - entrance only:
1=entrance - exit only:
1=exit
- both gates available:
- Permission:
Bird Flow Builder setup
- Preferred native-flow setup:
VoicetriggerAnswer callAsync HTTP request(initial IVR state fetch)Gather digits from a callAsync HTTP request(submit selected keys)- If the second HTTP response returns
completed=false, run anotherGather digits from a callwith the returned prompt and gather settings. - If the second HTTP response returns
completed=true, end the flow or optionally say the returnedmessage.
- The caller must be connected in Bird before the HTTP webhook step runs. If the flow omits
Answer call, the inbound call can keep ringing until Bird times it out. - Initial HTTP step:
- URL:
https://api.truckwash.io:4433/bird/voice/calls/webhook/inbound - Content type:
application/json - Body:
- URL:
{
"callId": "{{callId}}",
"channelId": "{{channelId}}",
"workspaceId": "{{workspaceId}}"
}
- Example native-flow response:
{
"requestId": "request-123",
"callId": "4015cf84-8028-46a1-a0d9-9213e5bf4f09",
"status": "gather",
"completed": false,
"stage": "department_select",
"prompt": "Choose department. Press 1 for Roskilde. Press 2 for Demo.",
"gather": {
"input": "dtmf",
"maxNumKeys": 1,
"endKey": "#",
"timeout": 30,
"retries": 3,
"say": {
"locale": "en-US",
"voice": "female",
"text": "Choose department. Press 1 for Roskilde. Press 2 for Demo."
}
}
}
- Use the initial HTTP step output to configure
Gather digits from a call:- Prompt/text from
promptorgather.say.text maxNumKeys,endKey,timeout, andretriesfromgather- TTS
localeandvoicefromgather.say
- Prompt/text from
- Selection HTTP step:
- POST the same
callId,channelId, andworkspaceIdplus the gathered digits askeys. - Example body:
- POST the same
{
"callId": "{{callId}}",
"channelId": "{{channelId}}",
"workspaceId": "{{workspaceId}}",
"keys": "{{gatheredKeys}}"
}
- Bird's current Voice API examples use BCP-47 locales such as
en-USand TTS voices such asfemale/male. Avoid undocumented values likealice. - Compatibility mode:
- The webhook still supports the older
{ payload, request, waitConditions }contract and returns a raw202 AcceptedBirdcallCommandgather envelope. - That mode is not suitable for Bird Flow Builder
Async HTTP request, because Bird treats the response as data rather than executing the returned call command.
- The webhook still supports the older
Numbers
-
GET /bird/numbers- Permission:
modules_bird_numbers_list - Optional query:
workspaceId(if not configured) - Query params are passed through to Bird.
- Permission:
-
GET /bird/numbers/{id}- Permission:
modules_bird_numbers_get - Optional query:
workspaceId(if not configured)
- Permission:
-
DELETE /bird/numbers/{id}- Permission:
modules_bird_numbers_delete - Optional query/body:
workspaceId(if not configured) - Releases/deletes a number if supported by Bird account policy.
- Permission:
Logging
Bird requests are logged through logs_o with module bird, including:
- Request start (
BIRD_HTTP_REQUEST) - Response status (
BIRD_HTTP_RESPONSE) - API or transport errors (
BIRD_HTTP_ERROR,BIRD_HTTP_CURL_ERROR) - Test outbound flow events (
BIRD_TEST_OUTBOUND_CALL_*)
Notes
- Bird credentials and base URL are configured in Bird module config.
- Route handlers enforce workspace/channel requirements before outbound API calls.