# 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](https://docs.masheev.com/capabilities.md)
and [authentication](https://docs.masheev.com/authentication.md). 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.

```json
{
  "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:

```json
{
  "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. Returns `requestId`.
- `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. Returns `messageId` and `conversationId`.
- `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-booking` and `cancel-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.
