Outbound calls and server workflows
Availability: off. outbound_calling, transactional_sms and outgoing_webhooks
remain disabled. These schemas describe preparation and future integration; they do
not authorize production calls, SMS or webhook delivery. See availability
and authentication. Organization API keys belong on your server.
Prepare a consented campaign
Owners and organization administrators can create campaigns and consent sources,
record consent, manage suppression and request higher limits. Members can read.
Platform review and verified seller identity are separate from organization setup.
Approved consent wording is immutable: create a new source to change it. An immutable
rejected source can be resubmitted with outbound.consentSources.submit and a stable
idempotencyKey. Replaying that key cannot reopen a later review decision.
Use outbound.status for current eligibility. Create a campaign, configure at most
three variants, add consented leads with outbound.requests.add, then preview with
outbound.preview (up to 1,000 phones per read-only batch) or outbound.check.
Previewing a draft treats its campaign as running, but preserves all other policy
checks. Preview does not reserve capacity or persist inline consent. Actual calling
requires enablement and rechecks current consent, withdrawal, ownership, windows and
limits immediately before dialing and answering.
Launch requires explicit confirmation of schedule and retry settings. Pausing stops new dials and cancels ringing/unsent attempts while preserving queued leads; active conversations finish. Resume waits until uncertain stop effects resolve. Completing or archiving cancels queued work; archiving retires snapshot agents for later provider cleanup. Canceling an explicitly requested callback leaves a human follow-up in its conversation. Request cancellation targets its exact current call.
Use stable per-row idempotency keys for enqueue retries. Public list imports also
deduplicate by campaign and phone. Trusted workflow calls use a separate logical
request per run/step, so two bookings for one person keep independent results.
Customer context and metadata never grant consent or callback authority.
Reconcile results
Read outbound.requests.list with updatedSince, follow every nextCursor, and
deduplicate by request ID. This is the reconciliation path after missed events.
Use outbound.calls.list and outbound.calls.get for public live-call state; provider
credentials, transport payloads and private consent/callback proofs are not public.
Events are call.completed, outbound.request.completed,
outbound.callback.scheduled and outbound.campaign.paused. They carry stable
eventId/occurredAt values and bounded, redacted customer metadata. A completed
request may have result: null; cancellation is not success. Call status can arrive
before extracted results; request completion waits for authoritative extraction or
the documented recovery backfill. Never infer a confirmed booking from a missing
boolean. Statistics exclude test calls; cost is billed USD (including recorded
billing shortfall), not provider conversation units. Conversion intervals are Wilson
95%; a winner requires at least 100 human answers per arm and non-overlapping intervals.
Send authenticated server events
POST /v1/events (also exposed as the events.ingest procedure) requires an
organization API key with write permission and matching orgId. Send from your
backend. Never invoke workflow webhook triggers from a browser or expose keys there.
{
"orgId": "your-organization",
"type": "booking.created",
"occurredAt": "2030-01-10T09:00:00Z",
"idempotencyKey": "booking-created-123-v1",
"subject": { "phone": "+442079460123", "externalId": "booking-123" },
"data": { "startAt": "2030-01-10T15:00:00Z" }
}
Supported types include booking.created, booking.updated, booking.cancelled
and custom.*. The subject phone must be valid E.164. Masheev resolves the contact;
do not supply authoritative contact, lineage or permission fields in data.
Inside a run, values are trigger.data.subject.externalId,
trigger.data.subject.phone, trigger.data.contactId and trigger.data.startAt.
accepted: true means durable admission; queued: false means publication awaits
recovery. Reuse the same key and identical payload after an uncertain response.
A durable cancellation revokes the exact organization's phone/booking reference. A stale update cannot revive it. Reusing a canceled external ID as a new booking requires an explicitly supported generation model; do not rely on such reuse today.
Durable waits and action outputs
Time waits accept { "until": "{{trigger.data.startAt}} - 3h" }. Event waits accept:
{
"for": {
"events": ["outbound.request.completed"],
"match": { "key": "requestId", "value": "{{steps.call.requestId}}" },
"timeoutMs": 7200000
}
}
SQL-backed waits resume from the stored workflow definition. A timeout produces
steps.<waitId>.timedOut; an event produces steps.<waitId>.event. The booking
result fields are flat: steps.call_result.event.data.metadata.result.confirmed
and .cancel_requested, not result.collected.
builtin:place_call:{campaignId, context?}. Uses the trusted booking subject, a service campaign, and booking-scoped transaction consent. ReturnsrequestId.builtin:send_sms:{inboxId, template:"booking_confirmation"}. Requires approved sender registration, transaction consent and no applicable opt-out. Uses a fixed service message with STOP footer. ReturnsmessageIdandconversationId.builtin:http_request:{connectionId, body}. Uses an owned active server-side signing connection. No raw URL or signing secret appears in the workflow step.- TablePort: only
sdk:tableport/get-booking,modify-bookingandcancel-booking. Booking identity comes from the trusted run; arbitrary foreign booking IDs fail.
Correlate SMS reply waits by the returned conversationId. Authenticated inbound
SMS events include channel:"sms", classification:"confirm"|"cancel"|"other",
and tenant/contact/inbox/conversation IDs. Several active booking conversations for
one recipient make a reply ambiguous; it is classified other, with
correlationAmbiguous:true. Do not guess a booking from free-text replies.
Booking template and external dependencies
The editable Booking confirmation (SMS → call) template waits until three hours
before the booking, reads latest booking state, sends SMS, waits 30 minutes, and calls
when there is no reply. Configure an approved SMS inbox, an always-on service campaign
with boolean confirmed and cancel_requested fields plus string reschedule_note,
and separate signed HTTP destinations for confirmation and staff follow-up. The
template rechecks latest booking/cancellation state before contacting the person.
TablePort is a separate company and deployment. This Masheev implementation does
not install a TablePort publisher, grant access to TablePort data, modify TablePort
code, or establish contractual coverage. TablePort must independently implement its
authorized server event publisher. Its current Masheev adapter has no confirmed-status
mutation contract; the template uses a configured signed HTTP confirmation action.
A receiving service must implement that contract before the branch can complete.
Do not add an invented status:"confirmed" parameter to modify-booking.
Signed destinations must be dedicated public HTTPS origins. Each request resolves
DNS and pins a checked public IP while validating TLS for the original hostname;
Cloudflare-proxied destinations are unsupported by the Workers socket transport.
There are no redirects or unpinned fallback, a ten-second deadline, and a 64 KiB
response limit. Verify X-Masheev-Signature (sha256= HMAC-SHA256 over exact UTF-8
body bytes) and deduplicate the stable Idempotency-Key on the receiver. Mutation
outcomes that cannot be proven are retained for reconciliation, not blindly repeated.
Isolated staging rehearsals before public enablement
Public outbound_calling remains off. After explicit owner approval of the test
number, transfer destinations and costs, an operator may configure
OUTBOUND_REHEARSAL_GRANT in an isolated staging environment. This is an operator
configuration, never a public request parameter. No grant is supplied by this
repository, and configuring one does not establish the separate launch approvals.
The strict JSON grant contains id, orgId, campaignId, variantId,
phoneE164, UTC issuedAt/expiresAt, and transferTargets entries containing
ruleId and to. Its lifetime must be at most two hours. Only
ENVIRONMENT=staging accepts it; production, development, missing, expired and
mismatched grants fail closed. Preparation requires a campaign with exactly one
non-stopped variant, matching the grant. OTP verification is limited to the exact
granted number and retains the ordinary verification rate limits.
Only the exact campaign/variant's verified source=test requests may use the
exception. Normal API, list and workflow requests remain unavailable. The org
still needs beta enrollment, approval, paid access, current outbound attestation
and seller identity; kill switches, suspension, suppression, country, caller
ownership, billing and capacity controls remain authoritative. Verified test
calls retain their documented consent/window exception. Claim, answer and
transfer read current authority; transfer rule and destination must both match.
Expired grants always fail closed. Removing the grant affects operations handled
by Workers running the updated configuration; it does not mutate an existing
isolate's environment. Fresh primary workspace suspension is the existing
immediate operator control. End an already connected call through call controls.
The live harness accepts --staging-rehearsal only with the repository-configured
https://api.staging.masheev.com origin. That flag skips only its redundant global
status check; the server authorizes every operation. The normal harness keeps
that check. Owner-number matching and both cost acknowledgements are still
required. Grant contents are excluded from public status, logs and receipts.
Staging's scheduled triggers remain disabled in Wrangler until isolated database and provider readiness is established. An authorized operator must arrange that readiness and normal scheduler/recovery operation before rehearsing. This code change enables neither schedules nor provider resources, sends no calls, and provides no live proof by itself.
Both transfer rehearsal scenarios require --rule-id and validate its destination
against the owner-approved transfer number. For the secondary path, request the
approved human handoff and have the agent use end_call before the watchdog fires;
only observed redirect proof passes. Instructions alone are not evidence.
Run scenarios separately as person-level redial gates permit, issuing separate
short-lived operator grants when necessary rather than relaxing those gates.