# drovr-api

Event ingress and observability for drovr journeys — versioned lifecycle
machines that move a tenant's contacts through email courses and
sequences.

## Feature flag catalog

Public definitions only. Catalog defaults are not serving values. Evaluation
is not wired; existing legacy authority remains unchanged. Nothing here
changes a flag or activates behavior. Follow each inspect link for its definition.

```json
[
  {
    "authority": "legacy-worker-binding",
    "catalogDefault": "shadow",
    "description": "Fold Kit inbound into actors rather than recording shadow evidence only.",
    "evaluation": {
      "reason": "not-wired",
      "revision": null,
      "servingValue": null
    },
    "key": "kit-webhook-fold",
    "lifecycle": {
      "kind": "release",
      "created": "2026-10-04",
      "owner": "drovr desk",
      "removalOutcome": "Remove the Kit fold/shadow branch after the Kit-retirement lane qualifies retirement. The Kit exit lane owns the removal and the owned-writer climb is pending, so the date moved from 2026-11-03 to 2026-12-01.",
      "removeBy": "2026-12-01"
    },
    "next": {
      "inspect": "/feature-flags/inspect?key=kit-webhook-fold",
      "list": "/feature-flags"
    },
    "scopeSchema": {
      "dialect": "draft-2020-12",
      "schema": {
        "type": "object",
        "properties": {
          "_tag": {
            "type": "string",
            "enum": [
              "global"
            ]
          },
          "stage": {
            "type": "string",
            "minLength": 1
          },
          "tenantId": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "_tag",
          "stage",
          "tenantId"
        ],
        "additionalProperties": true
      },
      "definitions": {}
    },
    "valueSchema": {
      "dialect": "draft-2020-12",
      "schema": {
        "type": "string",
        "enum": [
          "shadow",
          "fold"
        ]
      },
      "definitions": {}
    }
  }
]
```

## When to use this

- Post a tenant's contact events (signup, progress, completion) so the
  contact's journey advances: `POST /events`.
- Post up to 100 events in one request when you hold many (a backfill, an
  import): `POST /events/batch` with `{events: EngineEvent[]}`. Each
  item gets its own `accepted` / `rejected` / `failed` result, so one
  bad event never blocks the rest; replay the `failed` ones.
  Failed items may include optional `retryClass`: `d1-budget`, `d1-overload`, `do-reset`, `store-overload`, `unknown`.
  A healed `do-reset` need not reduce the rate; D1 pressure and unknown
  failures call for a steadier rate. Honour each item's replay wait.
- Seed one resumable page of AI Hero contacts into the contact directory:
  `POST /journeys/contact-directory/seed` with `{afterId?, createdAfter?, limit?, dryRun?}`;
  the response hands back the next cursor and link. Set `dryRun: true` to
  exercise the Hyperdrive read without touching actors. The route is
  read-only against AI Hero and idempotent in drovr.
- Resolve up to 1000 Kit subscriber ids to AI Hero contact ids:
  `POST /journeys/contact-directory/identities` with
  `{kitSubscriberIds: string[]}`. The route reads only the Kit provider
  identity table and returns matched `{kitSubscriberId, contactId}` pairs.
- Read a tenant's committed transition log, filtered by contact or
  journey: `GET /receipts?contact=&journey=&limit=`.
- Trace one contact across facts, folds, dispatch attempts, receipts, alarms,
  and wakes: `GET /contacts/trace?contact=&journey=&since=&cursor=`. Add
  `confirm=actor` to read actor state, wakes, and retries without redriving.
- Read armed wakes, or only the ones that are past due and still armed:
  `GET /wakes?contact=&journey=&overdue=&limit=`.
- Re-arm actors whose alarm was dropped, by exact identity or every overdue
  one: `POST /wakes/poke` with `{actors}` or `{overdue, limit?}`. A cron
  sweep does the same every 10 minutes for wakes overdue by 10 minutes.
- Review a bounded evidence window: `GET /review?since=2h&until=&compare=previous`.
  The summary stays under 8 KiB and links each finding to a metric drill-down.
- Inspect one closed metric: `GET /metrics/{wakes|dispatch|folds|storage|queries|delivery}?since=&until=`.
  Missing operator capability is reported as unavailable or unsupported, never zero.
- Read the three-light status (flow, stuck with actor ids, served):
  `GET /status?deadline=&window=`. Start here for "is anything wrong".
- Read seven vitals, each with its state, a 24-hour spark, what it is and why
  it matters now; the headline is the worst known of them. Vitals not
  measured yet are named, never graded; a vital whose source couldn't be
  read makes the headline at least amber. Its sends section answers "how are
  sends going?" per provider, against the day before: `GET /vitals`.
- List the intents behind the owed-completions vital (stranded, never
  dispatched), oldest first: `GET /stranded-intents`.
- List every standing pause (a journey hold today): its terms or what is
  missing, the release condition read now, and the work it parks:
  `GET /pauses`.
- Check the service and the journeys it serves, no auth: `GET /health`.
- The optional versionId in GET /health reports the running Worker version when metadata is bound; absent metadata omits the field.

All routes: GET / · GET /llms.txt · GET /openapi.json · GET /.well-known/mcp.json · GET /favicon.ico · HEAD /favicon.ico · GET /.well-known/agent-configuration · GET /.well-known/oauth-protected-resource/mcp · GET /.well-known/oauth-authorization-server/api/auth · GET /oauth/device · POST /oauth/device · GET /oauth/tenant · POST /oauth/tenant · GET /oauth/consent · POST /oauth/consent · GET /health · GET /api/auth/* · POST /api/auth/* · GET /contacts?contact=&journey= · GET /contacts/audit?tenantId=&contactId=&journeyId= (operator) · GET /contacts/trace?contact=&journey=&since=&cursor=&confirm=actor · POST /contacts/redrive?contact=&journey= · POST /contacts/completion-acceptance (operator) · POST /contacts/restart-course (operator) · POST /events · POST /events/batch · POST /events/redrive (operator; keys max 50, actors max 5) · POST /events/redrive/parked/pass (operator) · POST /signups · POST /contact-sync/heartbeat · POST /journeys/contact-directory/seed (body: {afterId?, createdAfter?, limit?, dryRun?}) · POST /journeys/contact-directory/identities (body: {kitSubscriberIds: string[] up to 1000}) · GET /receipts?contact=&journey=&limit= · GET /scheduled-health?slots= (operator) · GET /actor-log/errors?from=&through=&versionId= (operator) · GET /deployment (operator) · GET /request-traffic?report=&from=&to=&route=&release=&tool= · GET /email-delivery?contact=&email= · GET /content/messages · GET /content/messages/{messageId} · GET /content/messages/{messageId}/versions/{version}/render · GET /content/publishes?message=&status=&limit= · GET /content/pushes/{commit} · POST /content/messages/new · POST /content/messages/validate · GET /corpus/pushes/{visibility}/{commit} · GET /corpus/search?query=&mode=&types=&limit=&rerank= · GET /corpus/sources/{id} · GET /tenants/{tenant}/actors?cursor=&limit= (operator) · GET /tenants/{tenant}/directory-stops?cursor=&limit= (operator) · GET /tenants/{tenant}/unfolded-events?since=&until=&scope=&cursor=&limit= (operator) · POST /tenants/{tenant}/actors/retire (operator; max 200) · POST /tenants/{tenant}/content/messages/{messageId}/versions (operator) · POST /tenants/{tenant}/content/pushes/{commit}/reprocess (operator) · POST /tenants/{tenant}/content/repo (operator) · POST /tenants/{tenant}/corpus/repo (operator) · POST /tenants/{tenant}/links/targets (operator) · POST /tenants/{tenant}/migrations/{planId}/dry-run (operator) · POST /tenants/{tenant}/migrations/{planId}/apply (operator) · POST /tenants/{tenant}/migrations/{planId}/rollback (operator) · POST /tenants/{tenant}/provision (operator) · POST /tenant-keys/create (operator) · POST /tenant-keys/list (operator) · POST /tenant-keys/revoke (operator) · POST /tenants/{tenant}/profile-sources/{journey} (operator) · POST /tenants/{tenant}/provider-ramps/{journey} (operator) · POST /provider-sync/requeue (operator) · POST /value-path/stops/backfill (operator; max 50) · POST /value-path/team/enroll (operator; one contact; sends) · POST /confirmations/resend (operator) · GET /intake/signups?tenantId=&formId=&cursor=&limit= (operator) · GET /feature-flags · GET /feature-flags/inspect?key=&tenantId= · GET /review?since=&until=&compare=previous · GET /metrics/{name}?since=&until= · GET /wakes?contact=&journey=&overdue=&limit= · POST /webhooks/axiom (Axiom bearer) · POST /webhooks/front (Front-signed) · POST /webhooks/kit/{tenant} (Kit-signed) · POST /webhooks/postshiba (PostShiba-signed) · POST /wakes/poke (body: {actors: [{contactId, journeyId}] up to 50} or {overdue: minutes, limit?: up to 200}) · GET /vitals · GET /status?deadline=&window= · GET /births?journeyId=&cursor=&since=&limit= · POST /get_contacts · GET /contact-deliverability?tenantId=&contactId= (operator) · POST /resolve_contacts · GET /suppressions?scope=&since=&contactId=&activeOnly=&includeCounts=&cursor=&limit= · GET /delivery-events?since=&until=&reputationOnly=&cursor=&limit= · POST /add_broadcast_recipients · POST /dry_run_broadcast_audience · POST /create_planned_broadcast · POST /edit_broadcast_plan · POST /broadcast_status · POST /broadcast_recipients · POST /cancel_broadcast · POST /create_broadcast · POST /edit_broadcast_content · POST /edit_broadcast_caps · POST /edit_broadcast_in_flight · POST /edit_broadcast_pace · POST /edit_broadcast_pool · POST /edit_broadcast_send_errors · POST /pause_broadcast · POST /reschedule_broadcast · POST /resume_broadcast · POST /retry_uncertain · POST /schedule_broadcast · POST /export_campaign_cohort · POST /grow_campaign_cohort · POST /materialize_interest_cohort · POST /record_interest_facts · POST /seed_campaign_cohort_from_broadcast · POST /campaign_cohort_status · POST /campaign_engagement · POST /control_sharded_broadcast · POST /create_sharded_broadcast · POST /sharded_broadcast_status · GET /conversion?journey=&from=&to=&anchor=&until= · GET /journey-funnel?journey=&from=&to= · GET /signup-counts?from=&to= · GET /journey-health?journey= · GET /send-path?tenantId=&journeyId=&contactId= · GET /send-status?tenantId=&at= · GET /deliverability-health?tenantId=&at= · GET /campaign-report?tenantId=&campaignId=&interestCohortId= · POST /broadcast_engagement · GET /event-trace?tenantId=&contactId=&journeyId=&idempotencyKey= · GET /kit-subscriber-stops?kitSubscriberId= · GET /audience-total · GET /contact-list-state?contactId=&kitSubscriberId= (exactly one) · POST /backfill_contact_identities (operator, dryRun defaults true) · POST /product-events · POST /product-interest/dry-run · GET /next-cta?placement=&contactId= · GET /contact-segments?contactId= · GET /segment-coverage?tenantId= · POST /cut-facts/preview · GET /cta-policy · POST /cta-policy · POST /journey-funnel/rebuild · POST /journey-funnel/replay-clicks · GET /stranded-intents · GET /pauses · POST /unsubscribe · GET /unsubscribe?t= · GET /unsubscribe/state?t= · POST /confirm · GET /confirm?t= · GET /confirm/world?t= · GET /confirm/egghead?t= · GET /confirm/state?t= · POST /confirm/resend · POST /consent/begin (operator; writes Held, sends Held) · POST /consent/drain (operator; mysql mode, D1 epoch Held at the pinned generation) · POST /journeys/{id}/hold · DELETE /journeys/{id}/hold · GET /journeys/{id}/hold · POST /mcp (MCP streamable HTTP, protocol 2026-07-28)

Every route's inputs, outputs, and refusals as OpenAPI 3.1, derived from
the same Effect Schemas the handlers decode with: `GET /openapi.json`.

## Auth

Authorization: Bearer <api key> — per-tenant keys, operator-minted.
Keys are handed out-of-band by the drovr operator; there is no machine signup
endpoint.

A key's role decides what it may do. Read routes need `read`: `GET /audience-total`, `GET /births`, `GET /campaign-report`, `GET /contact-list-state`, `GET /contact-segments`, `GET /contacts`, `GET /contacts/trace`, `GET /content/messages`, `GET /content/messages/{messageId}`, `GET /content/messages/{messageId}/versions/{version}/render`, `GET /content/publishes`, `GET /content/pushes/{commit}`, `GET /conversion`, `GET /corpus/pushes/{visibility}/{commit}`, `GET /corpus/search`, `GET /corpus/sources/{id}`, `GET /cta-policy`, `GET /deliverability-health`, `GET /delivery-events`, `GET /email-delivery`, `GET /event-trace`, `GET /journey-funnel`, `GET /journey-health`, `GET /journeys/{id}/hold`, `GET /kit-subscriber-stops`, `GET /metrics/{name}`, `GET /next-cta`, `GET /pauses`, `GET /receipts`, `GET /request-traffic`, `GET /review`, `GET /send-path`, `GET /send-status`, `GET /signup-counts`, `GET /status`, `GET /stranded-intents`, `GET /suppressions`, `GET /vitals`, `GET /wakes`, `POST /broadcast_engagement`, `POST /broadcast_recipients`, `POST /broadcast_status`, `POST /campaign_cohort_status`, `POST /campaign_engagement`, `POST /content/messages/new`, `POST /content/messages/validate`, `POST /export_campaign_cohort`, `POST /get_contacts`, `POST /product-interest/dry-run`, `POST /resolve_contacts`, `POST /sharded_broadcast_status`, `POST /journeys/contact-directory/identities`.
Operate routes need `operate`: `DELETE /journeys/{id}/hold`, `POST /add_broadcast_recipients`, `POST /cancel_broadcast`, `POST /contact-sync/heartbeat`, `POST /contacts/redrive`, `POST /control_sharded_broadcast`, `POST /create_broadcast`, `POST /create_sharded_broadcast`, `POST /edit_broadcast_caps`, `POST /edit_broadcast_content`, `POST /edit_broadcast_in_flight`, `POST /edit_broadcast_pace`, `POST /edit_broadcast_pool`, `POST /edit_broadcast_send_errors`, `POST /events`, `POST /events/batch`, `POST /grow_campaign_cohort`, `POST /journey-funnel/rebuild`, `POST /journey-funnel/replay-clicks`, `POST /journeys/{id}/hold`, `POST /materialize_interest_cohort`, `POST /pause_broadcast`, `POST /product-events`, `POST /record_interest_facts`, `POST /reschedule_broadcast`, `POST /resume_broadcast`, `POST /retry_uncertain`, `POST /schedule_broadcast`, `POST /seed_campaign_cohort_from_broadcast`, `POST /signups`, `POST /wakes/poke`, `POST /journeys/contact-directory/seed`. Every key handed out
today holds both; a read-only key gets `tenant-permission-denied` (403) on an
operate route. Owner routes need `owner`: `POST /cta-policy`. Only a key
minted with the owner role, or an owner's own session, holds it; any other caller
gets `tenant-permission-denied` (403).

Every route but `GET /health` is rate limited, per API key (reads and
operates count separately) and per client IP (requests without a key, and
`/api/auth/*`). A refused request gets `rate-limited` (429) with a
`Retry-After` header: wait that many seconds, then resume at a steadier
rate. `GET /` lists each limiter's window as `rateLimits`.

`api-key-store-unavailable` (503) means the key could not be checked, not
that it was refused: the key store could not be read. Wait the
`Retry-After` seconds and send the same request again.

MCP clients can sign in instead of holding a key: `POST /mcp` with no
credential answers 401 with `WWW-Authenticate` naming
`/.well-known/oauth-protected-resource/mcp`, and from there the client
follows OAuth (PKCE, a Client ID Metadata Document) to an access token
bound to one tenant and to this host's `/mcp`. In the browser the person
signs in at `/oauth/tenant`, picks the tenant, and allows the client at
`/oauth/consent`. A read grant is capped at viewer; `tenant:operate` is
capped at operator. Owner-only capabilities require explicit `tenant:owner` consent
and live owner membership; an operate-only grant never owns. Tokens work
only at `POST /mcp`; REST routes refuse them.

A CLI signs in with the device flow (RFC 8628): `POST /api/auth/device/code`
answers a code; the person opens `/oauth/device`, enters it and picks the
tenant; the CLI then redeems its device code at `/api/auth/oauth2/token`.

Browser sign-in uses Better Auth sessions instead of tenant API keys. Its
routes live under `/api/auth/*`, and only the configured email allow list
may sign in.

## Events

`POST /events` takes a JSON EngineEvent:

```json
{
  "contactId": "contact-123",
  "idempotencyKey": "contact-123:contact.created:2026-08-30",
  "journeyId": "value-path-skills-course",
  "occurredAt": "2026-08-30T00:00:00.000Z",
  "tenantId": "org-your-tenant",
  "type": "contact.created"
}
```

Delivery is at-least-once: a replay with the same `idempotencyKey`
is safe and returns `appended: false`.

`POST /events/batch` takes `{events: [EngineEvent, ...]}` (1 to 100)
and answers `{accepted, rejected, failed, results}`, one result per
item in order. Same at-least-once rule per item.

`ingress-transient` (503) on `POST /events` means the platform failed the
call to the contact's actor for a moment: the event is logged, nothing was
refused, and its fold is not confirmed. Wait the `Retry-After` seconds and
send the same request again; the fold dedupes on `idempotencyKey`. A batch
item in that case comes back `failed`, never `rejected`, and its detail
names the wait: replay it after that many seconds. An `ingress-failure`
(500) is any other failed forward, and a retry is safe there too.

## Diagnosing

To stop a journey right now: call
`hold_journey` with the journey and a reason. Call
`release_journey` to let parked intents resume.

For "why didn't this contact get an email":

1. Call `trace_contact` with the contact and journey for
   the ordered projection timeline.
2. Add `confirmActor: true` when projection evidence needs actor truth.

A pending state with a receipt whose `intentsEmitted` is greater than
zero means the fold handed work to an executor and now waits for a
completion event. It does not prove that the provider accepted delivery.

An empty `wakes` list on a nonterminal state means no time edge is
armed. The actor must receive an external event to move; if no valid event
can arrive, it is stuck.

For "who in a cohort bought", call `conversion_funnel`
with the journey, a cohort window, and `anchor` (for the evergreen offer,
`coupon.issued`); pass the offer's expiry as `until` to count only
purchases before it. `purchased.contacts` is every buyer the offer
stopped for, at any price; `purchased.attribution.coupon` is the ones
whose purchase carried the tenant's coupon fact (offer conversions), and
`productMatched` the rest. AI Hero sends the coupon fact only on
purchases from 2026-09-29 ~18:00Z on: a cohort that bought before then
reads coupon 0 even when its coupons sold, so read its redemptions from
the tenant's own records, never as "the offer didn't sell".

For "is anything due that did not fire", call `get_wakes`
with `overdue` set to a grace period in minutes. A wake listed there is
past its `firesAt` (the due time `at` plus the contact's spread of up to
30 minutes) and the actor still reports it armed: the alarm did not run
it. A wake between `at` and `firesAt` is on schedule, not late. Confirm with `get_contact` before acting; the wakes
projection is best-effort and the actor is the truth.
To repair one, call `poke_wakes`: it re-arms the actor's
alarm and the alarm runs the wake. A poke that answers `healthy` found an
alarm already armed.

## MCP

MCP clients speak JSON-RPC to `POST /mcp` (streamable HTTP, protocol
2026-07-28 only — stateless, no initialize handshake; start with
`server/discover`). Auth rides the same `Authorization: Bearer
<api key>` HTTP header on every request. The tools:

- `apply_migration` — Operator-only: move up to 200 exact contacts to a registered migration plan's release, each through its own actor, only when dryRunId names a dry run of this plan's current sha for this tenant, at most 24 hours old, in which every listed contact would migrate. A retiming plan needs a per-call sign-off that does not exist yet, so it is refused, as is a blocked plan. Safe to repeat: a migrated contact answers alreadyMigrated. Requires the OPERATOR_TOKEN bearer.
- `backfill_send_acceptance` — Operator-only: mark one existing uncompleted PostShiba email send as accepted using stored actor.dispatch evidence for the exact tenant/contact/journey/intent key since the supplied instant. Refuses non-PostShiba, conflicting, ambiguous or missing evidence. dryRun:true writes nothing. Apply marks optional outbox evidence and arms the actor alarm; it never calls the executor or sends. The actor completes unconfirmed at max(original dispatch +24h, now). Requires OPERATOR_TOKEN and separate production data-write approval.
- `backfill_value_path_stops` — Operator-only: stop the listed contacts' value-path actors while each contact still has an active all suppression, re-read when the call runs. Folds contact.unsubscribed under the same key the tenant-stop fan-out uses, so a repeat or a later fan-out folds nothing. The stop emits no intent. dryRun:true counts the actors by version and state and writes nothing. Stops after more than 3 failed contacts. Requires the OPERATOR_TOKEN bearer.
- `begin_consent_inbox` — Operator-only: open a new consent inbox cycle through the existing guarded D1 CAS, with explicit previous cycle counters, revision and generation, next cycle identity and main epoch generation. Requires writes Held and sends Held, and returns the source cycle readback. Never changes write or send admission, calls a provider or resets pending facts. Invoke before the frozen Worker hold. Requires OPERATOR_TOKEN.
- `campaign_engagement` — Read how a campaign's mail is landing, counts only, never a contact. Pass the source campaign (campaignId), the interest cohort a materialize_interest_cohort run filled for it (interestCohortId), up to 8 lane cohort ids, and openHistoryCampaignId for the dormant group. Answers, each section with its asOf and source: messages (per message the run reads clicks from: recipients summed over its versions, and distinct contacts with a person click, by link role from the run's link roles), scoreBands (the run's bands, cutoffs, policy revision and finishedAt), buyersByBand (buyers per band by pre-purchase score, from the same run), lanes (members, and recipient rules by effect, subject kind and reason), reach (distinct contacts mailed any message), noScore (mailed contacts with no run signal, and their buyers) and dormant (older-only Kit openers with no campaign signal). messages, reach, noScore and dormant come from the engagement rollup's last snapshot, which a first rollup takes minutes to count; while no fresh snapshot exists, messages are read at call time and reach, noScore and dormant read unknown with the rollup's progress. A section the store or the run didn't answer is unknown, never zero. nextMoves names the reads behind the numbers. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `conversion_funnel` — Read one cohort's funnel for a journey: the contacts that entered in a window (born there, or folding the anchor cause, e.g. coupon.issued for the evergreen offer), how many reached each state after it, how many clicked each message's drovr links (clicks no scanner signal flagged), and how many bought: every purchase that stopped the offer (took its purchase transition) before until, by the step they bought at and by UTC day, split into coupon (the purchase carried the tenant's coupon fact: an offer conversion) and productMatched (product-matched, unattributed: a regional or full-price buy, or any purchase without the fact). AI Hero sends the coupon fact only on purchases from 2026-09-29 ~18:00Z on, so an earlier cohort reads coupon 0 even when its coupons sold; its redemptions are in the tenant's own records. Counts only: revenue and refunds wait on versioned outcome events. Read-only, from receipts, the buyers' purchase events and link clicks. The cohort window is at most 2 days and until at most 30 days past it; a wider one is refused. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `count_unfolded_events` — Operator-only: count one page of a tenant's logged events (drovr_events) that no fold is known to have taken, over a received-at window (since at most 13 days ago, until at least 1 hour ago). An event whose actor has no receipt is never-born: refused (the actor logged a cold-start refusal for it) or stranded (no refusal line). scope full also checks each born actor's event for a receipt naming it. Pages read a fixed number of event-log rows across all tenants, so the cost stays bounded; pass nextCursor back until it is null and sum the counts. Read-only. Requires the OPERATOR_TOKEN bearer.
- `create_tenant_key` — Operator-only: mint a tenant API key with a name and read or operate role. Returns its value exactly once; never log or persist the response. Requires OPERATOR_TOKEN, never a tenant key or browser session. The tenant must already be provisioned with an owner.
- `describe_surface` — Describe this service: routes, journeys, auth, the example event. Start here. Over MCP it needs a credential, as every MCP call does: an API key or an OAuth access token. GET / over REST needs none.
- `drain_consent_inbox` — Operator-only: apply one explicitly bounded page of the captured D1 consent inbox to MySQL, replaying original committed receipts before marking source facts applied. Requires pinned cycle and main epoch generation, an explicit page bound and measurement reference, MySQL writes Open and sends Held. Reports fresh source counts and an opaque continuation; closes the source cycle only when its accepted and applied counts reconcile. Never releases sends or calls a provider. Requires OPERATOR_TOKEN.
- `dry_run_product_interest` — Score every contact's product facts (the product-facts scope record_product_events fills) under an interest policy and return the distribution: score bands, contacts at or above each cutoff, buyers and unknown ownership. Counts only, never a contact. Nothing is written, so try weights, half-lives and band edges freely; the first real numbers come from here. Refused when the scope holds more facts than one request reads, when the policy names another tenant, or when an excluded product has no buyers-of deny rule on the policy's campaign. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `enroll_value_path_team` — Operator-only: move one contact's running value-path actor from the individual track onto AI Hero's team track, starting at team email startAt (default 1), and send that email. Only a v13 actor waiting between individual emails moves: an email in flight answers inFlight, a finished course answers finished, a team-track actor answers alreadyOnTeam, and an older actor answers needsMigration with the plan to run first (plan_migration, then apply_migration). Nothing already sent is sent again: the team emails are new content, and the individual drip ends. A repeat folds nothing. dryRun:true reads and answers without writing. Requires the OPERATOR_TOKEN bearer and sends real email.
- `explain_send_path` — Explain the read-only send gates for a tenant and journey, with pass, fail or unknown evidence and next moves. An optional contactId evaluates its actual routing cohort without pinning it. Tenant keys may inspect only their own tenant; the operator bearer may inspect any tenant. No send or provider write. This is current configuration evidence, not a promise that a later send will succeed.
- `flip_feature_flag` — Move one catalog flag one rung for a scope, or "off" to its declared safe rung. A climb is refused unless the caller's authority covers the rung (OPERATOR_TOKEN is agent; FLAG_ARBITER_TOKEN is arbiter; the arbiter token plus approvalRef is joel) and the current rung's exit evidence is recorded as met (record_flag_evidence). Off is allowed at any authority, except from a rung that refuses it. Writes the value to the stage's flag store and a receipt to the flag's ledger; read_flag_ledger reads them back. The scope must be one the flag's consumer reads; platform flags refuse other scopes with the corrected call. A refusal names the valid next moves. Other locations see the value within about 60 seconds, so this is never the first kill switch.
- `get_audience_total` — Read your tenant's last completed mailable audience census: confirmed profiles without holds, with owned identity and without active all-scope suppression after lifts. One stored read, never an on-demand scan. asOf is the census start, not a claim of current or transactional membership; a stale census is returned unchanged. Not qualified for broadcast sizing: the current refresh can leave normal asOf age near 42 hours. Missing or unreadable evidence is unavailable, never zero. Sends nothing and writes nothing. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_contact` — Read one contact's current actor state and armed wakes. Requires contact and journey. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_contact_deliverability` — Operator-only: read drovr's active stops and PostShiba's suppression reason/date for a tenant and contactId. Resolves the address server-side through ContactIdentity; no address appears in the response or logs. Provider/identity failure is unknown, never not-suppressed. Sends nothing and writes nothing.
- `get_contact_list_state` — Read one contact's list state: confirmed, unconfirmed, stopped or unknown, at scope all, with each active stop and its scope and each form's confirmed or unconfirmed consent. Name the contact by exactly one of contactId or kitSubscriberId; a Kit subscriber id resolves through the AI Hero directory, so only the AI Hero tenant may pass one. The tenant comes from authentication, never the input. An active all stop wins over any confirmation. No contact, or no consent evidence, is unknown with its reason, never on the list. Unavailable identity, consent or stop evidence is a typed retryable failure. No address in the response. Read-only: no send, provider write or suppression change. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_contacts` — Read a bounded batch of contact/journey pairs from the receipt projection. Each item returns projected state or missing (not proof the actor is unborn). Supply a value-path email id per item for delivery; delivery for other journeys is not supported. confirmActor opts into read-only actor confirmation, never redrive. Results preserve input order and duplicates; per-item unavailable does not discard other results. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_content_message` — Read one of your tenant's email messages: its current version in full (subject, preview, body, layout, sender), every stored version newest first with its source, its journey, and how many contacts are pinned to it. Render a version with render_content_version. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_content_push` — Read what one git push to your tenant's content repo did: each email file's outcome (cataloged, published with its version, refused with reasons, unchanged, superseded by a later push, or deleted-ignored). Cataloged is not published; next names publish_content with the message id for cataloged files. A push not yet processed reads retrying (the queue still retries it), requeued (a reprocess put it back on the queue) or failed (dead-lettered or dropped for a missing Artifacts setting), and next names the moves that fit. Pass the pushed commit's sha. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_corpus_push` — Read what one git push to your tenant's public or restricted corpus repo did: indexed (the head it converged on, counts, and each changed file: indexed, deleted, or refused with reasons), still indexing, or failed. Pass the repo's visibility and the pushed commit's sha. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_corpus_source` — Read one source of your tenant's corpus by id (as search_corpus answers it): its provenance, and its full text when it is public. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_deployment` — Operator-only: read every served Worker's live deployment, all traffic versions and percentages, source SHA and release marker from release-runner stamps (workers/tag and workers/message annotations, not verified provenance), and deploy time. Previous deployment evidence names none, unreadable with a reason, unstamped, mixed, or known SHA. Missing credentials or unreadable evidence is unavailable, never a guessed deployment. servingApi identifies this responding instance only; its timestamp is version creation, not deployment. Overall SHA is known only when every Worker agrees. Compare links replace private PR enumeration. Requires OPERATOR_TOKEN.
- `get_email_delivery` — Read whether drovr delivered one value-path email to a contact: delivered (with deliveredAt and the provider), pending (pinned to PostShiba and not delivered yet), not-routed (not sent through PostShiba: the Kit path owns it), or not-started (no value-path history in drovr). route is the provider the contact's sequence is pinned to. Three indexed MySQL reads; never wakes the contact's actor. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_feature_flag` — Inspect a public flag's catalog definition. Supply tenantId with an operator bearer token to read a tenant-scoped flag's effective FLAGS value, revision and evaluation reason. Legacy flags report Worker-binding authority, never KV; their value/revision stay null with reason legacy-binding-not-observed. Without tenantId, servingValue/revision stay null and operator flags stay private. Read-only; no writes or activation.
- `get_kit_subscriber_stops` — Resolve one Kit subscriber id and read its active all-scope stops in one request. AI Hero tenant keys only; the tenant comes from authentication, never the input. No matching contact or no active all stop returns an empty stops list. Unavailable or ambiguous identity/suppression evidence is a typed retryable failure, never empty. Read-only: no send, provider write or suppression change. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_receipts` — Read a capped latest sample of your tenant's committed transitions, newest first (default 50, maximum 200). This is not the complete log and has no pagination cursor. Optional filters: contact, journey, limit. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_review` — Review a bounded tenant evidence window. Returns folds, dispatch, wakes, explicit unavailable capabilities, and up to three drill-down findings. Optional: since, until, compare=previous. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_scheduled_health` — Read scheduled job evidence from durable Workflow instances for the last 1–12 closed ten-minute slots (default 12). Operator bearer only. Slots expose a run-evidence discriminator. lastRecordedRun and its lastRun compatibility alias come only from valid recorded per-job timing, not the latest Workflow status; inspect slots for newer pending or unavailable observations. Lag uses the producer's own scheduled timestamp. Returned means the job Effect returned, not proof of business success. Missing instances mean locally run or missed, never proof of a missed run. Jobs outside these Workflows have no durable source. scheduled.call.failed timeout counts are unavailable from Workflows, not zero. No Axiom query, replay, send or storage write.
- `get_status` — Read your tenant's three-light status: flow (completions in the window, per journey), stuck (actors in a pending state past the deadline with no wake armed, with ids), served (contacts active and receipts in 24h). owned is external: it needs your own entry log. Optional: deadline and window in minutes. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_vitals` — Read your tenant's seven vitals, the quickest answer to "is anything wrong": freshness, owed work, owed completions, failures, throughput, heartbeats and send quality. Each has a state (green, amber, red, or unknown when its source did not answer), a value, a 24-hour spark, and two sentences: what it is, and why it matters right now. headline is the worst known vital. A vital not measured yet (by design or too little data) is named, never graded; a vital whose source couldn't be read (unknownReason unreadable) makes headline at least amber, so a dead source never reads green. headline is unknown only when every vital is not measured yet. sends answers "how are sends going?": per provider, the last 24 h of dispatches (accepted, completed, failed, blocked, asked to retry later) against the 24 h before, deliveries, PostShiba bounces and complaints, blocks before any provider, emails waiting on a failed published check, owed completions, which journeys PostShiba carries, a summary and next moves; a count the HUD hub hasn't covered is null, never 0. context.owned says whether drovr dispatched this tenant's sends. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_wakes` — Read your tenant's armed wakes (future time edges), soonest first. Each wake carries at (the due time) and firesAt (due time plus the contact's spread of up to 30 minutes, when the alarm really fires). overdue=<minutes> returns only wakes still armed that many minutes past firesAt: those did not fire. Optional filters: contact, journey, limit. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `hold_journey` — Stop dispatching one journey for your tenant. Existing intents park and retry; wakes stay armed. Requires journey and reason. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `inspect_metric` — Inspect one bounded tenant metric: wakes, dispatch, folds, storage, queries, or delivery. Optional: since, until. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `journey_funnel` — Read a journey's daily funnel from the funnel projection: per cohort day (the UTC date of a contact's anchor, e.g. coupon.issued for the evergreen offer) and journey version, how many entered, clicked a drovr link no scanner signal flagged, and bought: boughtCoupon (the purchase carried the tenant's coupon fact) and boughtProductMatched (the rest). Definitions match conversion_funnel's one-day cohort with its default until. One indexed read of at most 92 days; a wider range is refused. The projection is derived from receipts, purchase events and link clicks and never gains authority over an actor; rebuild_journey_funnel repairs a day. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `journey_health` — One health read for a journey, every section naming its source and as-of time; a source that didn't answer reads unknown, never zero. funnel: today's cohort (entered, clicked, boughtCoupon, boughtProductMatched) against the mean of the 7 days before, from journey_funnel. stranded: exact journey counts and held counts from the HUD hub's whole-set intent groups, plus contact-level details from its bounded oldest-intent page. Older hubs fall back to page counts, marked partial when truncated. Held intents carry their recorded heldReason. stuck: actors in a pending state past the 60-minute deadline with no armed wake, by state and age across versions, from the hub's STUCK reading. overdueWakes: wakes still armed 10 minutes past firesAt. lastSend: the newest send completion among the tenant's newest 5000 receipts. rules: the flow and stuck alarm rules evaluated now (firing, quiet or unknown), with the numbers they read. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `list_births` — List the actors born on one journey for your tenant, a page at a time: contactId, journeyId, version and bornAt for each, in the order the receipt projection received their first receipts. Pass nextCursor back as cursor until it is null; then keep resumeCursor and pass it as cursor on your next poll to read only newer births. since starts the first page at an instant instead of the beginning. This is an index of births the projection has seen, eventually consistent with the actors, never their authority: asOf says how far the projection reached, so a contact missing from the feed is unknown until asOf is a grace period past its event. Read-only. Each page scans up to about 2,000 positions of the whole receipt log (every tenant's receipts). A page that lists fewer births than limit advances its cursor to the end of that slice; a page that fills limit stops at its last birth. So a sparse journey's pages cost receipt volume, not births: a since days back takes many pages per journey before nextCursor is null. Keep resumeCursor between polls and pass since only when you have no cursor, set to your last good read rather than a long lookback. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `list_content_messages` — List your tenant's email messages: every message a registered journey sends and every message ever checked, each with its journey, current version and subject, latest publish check, and pinned-contact count. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `list_content_publishes` — Read your tenant's content publish log, newest first: latest bounded checks, published with their versions or refused with their reasons. This is not the complete log and has no pagination cursor. Optional filters: message, status, limit. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `list_delivery_events` — List your tenant's PostShiba delivery events over a window, oldest first, a page at a time: kind (processed, delivered, deferred, bounced, blocked, expired, dropped, complained, unsubscribed), when it happened and arrived, the provider's message and event ids, attempt, the SMTP code, enhanced code and class (success, temporary, permanent), the receiving provider (gmail, google-workspace, microsoft, yandex, mimecast, mxroute, telekom, yahoo, other), the provider's IP (role unknown), the sending IP only when the reply text names it, whether it is a reputation refusal, and the reply with addresses masked. Never a recipient address. Pass nextCursor back as cursor, or follow next, until it is null. reputationOnly keeps only reputation refusals. Events are kept 90 days. Read-only. Reads the MySQL store; a deployment still on D1 answers delivery-events-unavailable. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `list_feature_flags` — List public feature flag catalog definitions and inspect links. Defaults are catalog defaults, never observed serving values. Evaluation is not wired; no tenant values, writes or activation.
- `list_form_signups` — Operator-only: page through an intake form's non-resend admissions in append order. Returns browser/agent source, consent/suppression state, timestamps and email domain only, never addresses or answers. Counts cover the whole form at this read, not just the page; pages are not frozen across calls. Requires OPERATOR_TOKEN. Refuses unavailable evidence or cohorts above 5000 rather than guessing or truncating counts.
- `list_pauses` — List every standing pause on your tenant's journeys, oldest first: what it stops (scope), when it was placed and how long it has stood, its recorded terms (owner, release condition until, reviewBy, reason) or Unrecorded when the hold carries only a reason, the release condition read now (Met, NotMet, or Unknown, which never releases), the terms still missing, the page rules it meets, and the intents it parks by kind with the oldest (from the HUD hub's ledger; Unknown when the hub does not answer, never zero). Today every pause is a journey hold placed by hold_journey; release_journey lifts one and sends the parked work. Read-only. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `list_stranded_intents` — List the intents behind the owed-completions vital: stranded (accepted or blocked, and still without a completion after the actor stopped re-asking) and never dispatched (every attempt threw). A block whose every reason is a suppression (suppressed:<scope> or postshiba:suppressed) is closed, not stranded, while the contact's suppression still covers the journey: suppressionClosedCount counts those, and a lifted suppression counts the block stranded again. A recorded hold carries heldReason (for example evergreen-held-for-exit); heldCount counts that subset of strandedCount before truncation. Held intents stay in the stranded list and count; missing heldReason is not evidence of a hold. Intents a journey hold parks before dispatch (every attempt answered held) are counted in parked by journey and kind, never as never dispatched; list_pauses names the hold behind them. Each has its kind, intent key, contact, journey, since and attempts, oldest first; counts are exact and the lists stop at the vital's limit (truncated). Intents still draining or retrying are counted, not listed. coveredFrom says where the HUD hub's ledger starts: an intent dispatched before it is unknown here. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `list_suppressions` — List your tenant's suppressions and their lifts, a page at a time: contactId, scope, the journey a course scope names, reason (bounced, complained or unsubscribed), createdAt, and whether a later lift ended it (lifted, liftedAt). Never an address. Pass nextCursor back as cursor, or follow next, until it is null; then keep resumeCursor (or resume) for your next poll to read only suppressions recorded since. scope narrows to one scope (all, course:<journeyId>, or course:* for every course scope); since keeps only suppressions created at or after an instant. contactId selects one contact across scopes; activeOnly returns only stops not ended by a later lift. Filters run in SQL before the page limit and seek across sparse positions. Without filters, each page walks about 2000 positions of the whole suppression log (every tenant's), so a page can list nothing and still carry a nextCursor. includeCounts adds exact active/lifted counts by scope over the whole tenant, independent of page filters; omit it to avoid aggregation. Lift state is as of this read: a lift or reapply on a row already read shows only when you read from the start again. resumeCursor can also miss a suppression whose write committed after a later one, so a sweep that must see every suppression reads from the start each run. Read-only. Reads the MySQL store; a deployment still on D1 answers suppressions-unavailable. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `list_tenant_actors` — Operator-only: list one page of a retired tenant's actor identities (contactId, journeyId) from the event log and the receipt and wake projections, for retire_tenant_actors. Pass nextCursor back as cursor until it is null. Pages come in the store's own order (raw contact then journey on D1, identity digest on MySQL), so walk to the end rather than relying on an order; each actor appears exactly once per walk. Requires the OPERATOR_TOKEN bearer and a tenant already listed in RETIRED_TENANTS.
- `list_tenant_keys` — Operator-only: list one tenant's key metadata from legacy MySQL and Better Auth. Never returns key values or hashes. Requires OPERATOR_TOKEN.
- `list_unsuppressed_directory_stops` — Operator-only: list one page of a tenant's stopped contact directories (unsubscribed, bounced) that have no scope-all suppression row, so the send gate would not refuse them. Read from the HUD hub's actor heads and checked against drovr_suppressions; nothing is written. coverage unknown means the hub's census may be incomplete, and the page proves nothing. Pass nextCursor back as cursor until it is null. Requires the OPERATOR_TOKEN bearer.
- `new_message` — Start a new broadcast in your tenant's content repo: answers an unused id (checked against the store and the repo head), its path (messages/<id>.md), a file skeleton to fill, and the archive index line to add (broadcasts/<yyyy>/<mm>.md). Nothing is written: commit and push the files. Sequence emails wait on the index-flip slice. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `open_content_repo` — Operator-only: open a tenant's content repo (every email as messages/<messageId>.md), creating it on first use, and mint one short-lived repo-scoped token: read to clone and fetch, write to also push. Answers the git remote, the token, its expiry, and the clone command. The token is never logged or audited. Requires the OPERATOR_TOKEN bearer.
- `open_corpus_repo` — Operator-only: open one of a tenant's two corpus repos (sources/ as markdown: public, or restricted for private and paid material), creating it on first use, and mint one repo-scoped token up to a year, for support's sync to push with. Answers the repo and its visibility, the git remote, the token, its expiry, and the clone command. The token is never logged or audited. Requires the OPERATOR_TOKEN bearer.
- `plan_migration` — Operator-only: dry-run a registered migration plan against up to 200 exact contacts' live actors, writing nothing to them: each would migrate (with the receipt it would write, its wakes, and how its next 90 days of intents would differ), is already migrated, or is refused with its reasons. Stores the report and returns its dryRunId, which apply_migration requires. Requires the OPERATOR_TOKEN bearer.
- `poke_wakes` — Re-arm actors whose alarm was dropped (Cloudflare retries a failing alarm only a few times, so a long storage outage strands overdue wakes). Send actors (1 to 50 exact {contactId, journeyId}) or overdue (grace minutes; pokes the actors get_wakes lists as overdue, up to limit, default 50, max 200). Each actor answers rearmed, healthy (an alarm already fires in time), idle (nothing owed), refused (retired tenant), or failed. A poke never folds or sends: the re-armed alarm runs the wake. Safe to repeat. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `post_event` — Post one tenant contact event; the contact's journey advances and {appended, receipts, intents, wakes} comes back. Delivery is at-least-once: replaying an idempotencyKey is safe and returns appended: false, and the event folded is the one first stored under that key, so to correct an event, send it under a new key. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `post_event_batch` — Post 1 to 100 tenant contact events at once (a backfill, an import). Each item gets its own result in input order: accepted (with what the fold committed), rejected (final; fix the event), or failed (transient; replay it). One bad event never blocks the rest. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `probe_flag_watch` — Operator-only, synthetic: the flag-watch-probe behaviour for a world-* tenant. With attribution flag it records one failure row naming the flag's key, revision and scope and this Worker version, but only while the flag is on; with attribution none it records one failure naming no flag. The flag watch in cd-checks turns the flag off for an attributed trip and pages for an unattributed one. Sends nothing and touches no real tenant. Requires OPERATOR_TOKEN and a stage with a flag store.
- `provision_tenant` — Operator-only: make one existing drovr tenant id a Better Auth organization, owned by someone who has signed in to the HUD once. Safe to repeat; it never renames a tenant, moves a slug, or changes a member's role. Requires the OPERATOR_TOKEN bearer.
- `publish_content` — Operator-only: publish one version of a tenant's email message once it passes its checks (the tenant's layout and sender, a subject and preview, every template reference in every branch, a strict test render, and every link on the tenant's allowed hosts). A refused publish is recorded with its reasons and changes nothing. Safe to repeat: the same content is the same version. Requires the OPERATOR_TOKEN bearer.
- `read_flag_ledger` — Read one flag's evidence and flip receipts, oldest first, and audit every KV page for that flag against its ledger, including scopes with no receipts. audit.attention is page when a revision is ahead, a same-revision value is changed, or integrity cannot be proved; these also emit exported Error signals. A lagging KV cache is not a violation. qualification appears when the flag's final rung has qualifiedBy evidence: QualificationPending lists what to record, QualificationMet clears it, and QualificationFailing (attention page) names the failing reads and the next move back to the off rung, and emits an exported Error signal. Requires OPERATOR_TOKEN or FLAG_ARBITER_TOKEN.
- `read_world_confirm_link` — Operator-only: read a simulated world contact's newest double opt-in confirmation link, the one the world mailbox line carried, so a world drive can click it. The link is minted again from the contact's committed double-opt-in receipts: the newest receipt that emitted the confirmation gives its issue time, and the same minter the send used signs it, so the answer is byte-identical to what was sent. Only a world tenant and one of its fixture forms are read; every other tenant is refused before any receipt is read or link minted. A contact with no confirmation receipt, or two newest at one instant, is refused rather than guessed. The link is never logged or stored. Requires the OPERATOR_TOKEN bearer.
- `rebuild_journey_funnel` — Rebuild one cohort day of a journey's funnel projection from the receipt log and the buyers' purchase events, replacing the day's members and keeping each member's stored click while it still falls in the rebuilt window. Rebuilding a day twice gives the same rows. Clicks come from the live click projection; run replay_funnel_clicks after rebuilding a range that predates it. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `record_flag_evidence` — Record one piece of exit evidence for a flag's rung: the event kind, the count a read observed, the read and a reference to re-read it. Supply the scope that was measured; unread platform scopes are refused with the corrected call. The newest record per event at the climb's scope counts toward the rung's exit thresholds. Requires OPERATOR_TOKEN or FLAG_ARBITER_TOKEN.
- `record_product_events` — Report up to 100 things known readers did in the tenant's product (an article read, video progress, a lesson completed, a pricing or team page viewed, a checkout step, a team intent), each keyed by drovr contact id. drovr stores them as interest facts, append-only and deduplicated, for the behavioral score and the next-best CTA. Send each event's at as the time the reader acted, close to when it is sent: an at more than 5 minutes ahead of drovr's clock, or older than 180 days, is reported in notStored, not stored. An id already stored for a different contact, kind, topic or page is reported as a conflict; the first fact stays. Refused unless the tenant's product-events flag is on; nothing is stored then. No email is sent and no contact changes. Call it with the tenant's API key from the product's server; a person's grant cannot report a product's stream. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `record_signup` — Record a double opt-in signup (#55): the contact lands unconfirmed and drovr emails the confirmation link; only the signed link's POST confirms. Idempotent per (tenant, contact, form); a repeat with a new submissionId asks for a resend (at most 3, 10 minutes apart). Answers awaiting-confirmation, already-confirmed, or suppressed (nothing is sent to a suppressed address). Where the tenant has re-subscribe enabled, a contact who only unsubscribed can re-subscribe through this fresh confirmation, once (a second is refused with resubscribe-limit); a bounced or complained address is always suppressed. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `record_stop` — Operator-only: record a reader's request to stop all email for one tenant by email address, never by a typed contact id. Resolve the address read-only, verify it through ContactIdentity, and post contact.unsubscribed to the contact directory through existing ingress. The suppression mirror and flagged tenant-stop fan-out run unchanged. Returns the active stop's original recorded time; repeating the call folds the same suppression episode and records nothing new. Unknown or ambiguous addresses are refused without writes. No provider calls or identity writes. Requires the OPERATOR_TOKEN bearer.
- `redrive_contact` — Re-ask the executor now for every intent this contact's actor dispatched whose completion never folded; returns how many completions folded and how many intents were re-asked. Executors are idempotent on the intent key, so a re-ask never sends twice. Requires contact and journey. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `release_journey` — Release one held journey for your tenant. Parked intents resume on their actors' retry alarms. Requires journey. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `render_content_version` — Preview one stored version of your tenant's email message: its subject, preview, HTML and text, rendered with sample values (listed in variables) through the layout it was published with. Nothing is pinned or sent. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `replay_funnel_clicks` — Apply one page of recorded link clicks (at most 500, in click id order) to your tenant's funnel members: a click no scanner signal flagged sets a member's first click when it falls between the member's anchor and its stage window's end. Start at afterId 0 and pass nextAfterId back until it is null. Safe to repeat. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `report_contact_sync` — Report the tenant's contact-sync watermark: every contact change up to syncedThrough has been pushed to drovr (contact.profile.updated, contact.links.issued, contact.offer.issued and the stop events) and acknowledged. drovr keeps the latest watermark and never moves it back; sends trust synced profiles only while it is recent. Call it with the tenant's API key after each sync run. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `reprocess_content_push` — Operator-only: re-enqueue a tenant's content push that failed (dead-lettered after its retries, or dropped while the Artifacts read token was missing), as get_content_push answers `failed`. The push runs again with the branch-head check, so only files main still holds are published. Answers `enqueued`, or `already-processed` when the commit has a processed row. A commit with no recorded failure is a 404. Requires the OPERATOR_TOKEN bearer.
- `request_traffic` — Read anonymous Basin request evidence over an ISO window of at most 24 hours: mcp-usage by tool and agent class, route-health with HTTP 5xx and p50/p95 latency by route and release cohort, discovery traffic, or requests by service. Only route-health fills errors, p50Ms and p95Ms; those fields are null by design in the other reports. Route-health groups by service, tenant, route and release, so a shared route and release can appear in separate service or tenant rows. Tenant-key authentication is sufficient and sees only the auth-derived tenant. An optional operator bearer expands the scope to all tenants and null-tenant traffic, labelled operator-or-unresolved, never inferred public. Optional route and tool filters come from this deployment, not observed usage; releases are observed within the caller's scoped window. Coverage carries those filters, deduplicated landed row count, newest receivedAt, neutral scan metrics, legacy host attribution and an estimated 300-second pending flush lag, not a visibility bound. Empty means no events landed, never zero traffic. Basin is evidence, not release authority. Zero claims belong to receipts (packages/events/README.md, Where a fact goes).
- `requeue_provider_sync` — Operator-only: re-queue dead provider-sync rows (a Kit list.unsubscribe refused while ai-hero lacked the kind, or any executor outage) by exact {idempotencyKey, target} rows or by kind (+ target, deadAfter, limit). Resets attempts; the next drain retries them. Rows already done are never touched. Requires the OPERATOR_TOKEN bearer.
- `resend_confirmation` — Operator-only: resend an existing pending Ratstack confirmation once per contact per 72 hours, keyed by tenant/contact/resendId. Never extends the original deadline. Retries use a frozen envelope inside the provider dedupe horizon; ResendIndeterminate requires operator reconciliation and never sends again. dryRun=true checks fresh eligibility/budget, reserves nothing and sends nothing; it returns no recipient/token. Unknown input fields are refused. Requires OPERATOR_TOKEN, not an intake credential or tenant API key.
- `resolve_contacts` — Resolve a bounded batch of email claims or kit:<id> aliases to your tenant's contact ids. Email normalization trims and lowercases only, preserving dots and plus tags. Per-item resolved, missing or conflict; conflicts never merge or expose candidate ids. kit aliases use the existing AI Hero directory identity bridge (1000/call), only for its configured authority tenant; other tenants get missing. Email lookup requires MySQL and a complete normalized-address index. Results preserve input order and duplicates. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `resolve_directory_identities` — Resolve up to 1000 Kit subscriber ids to AI Hero contact ids; returns the matched {kitSubscriberId, contactId} pairs. Only the AI Hero authority tenant (AIHERO_EXECUTOR_TENANT) may call it. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `restart_course` — Operator-only: restart one contact's finished course from its journey's entry, sending lesson one again, only when the learner asked for it. Every guard must hold, re-read when the call runs: the actor is in a final state, owes no completion, and its committed receipts prove that no course email was sent (any sent or unproven lesson refuses course-already-started); no active stop covers the contact and journey (an unsubscribe needs a fresh confirmed sign-up first; a bounce or complaint always refuses); and evidence names the learner's request. One restart per evidence: a repeat answers alreadyRestarted and does nothing. The new run's intents get fresh keys, so nothing from the old run is replayed or skipped. dryRun:true checks the guards and writes nothing. Requires the OPERATOR_TOKEN bearer and sends real email.
- `retire_tenant_actors` — Operator-only: permanently delete one bounded page of at most 200 exact actor identities for a tenant already listed in RETIRED_TENANTS. Requires the OPERATOR_TOKEN bearer.
- `revoke_tenant_key` — Operator-only: revoke one tenant key by Better Auth id or legacy seq, retaining its row. Safe to repeat. A preflight refuses the last active key unless confirmLastKey:true because that tenant's callers start getting 401. Concurrent revokes can race the preflight. Cached authentication expires within 60 seconds; direct verifier reads refuse immediately. Requires OPERATOR_TOKEN.
- `rollback_migration` — Operator-only: undo a registered migration plan for up to 200 exact contacts, each only while nothing has folded since its migration; a contact that has moved on refuses folded-since-migration, and undoing it is a forward migration instead. Safe to repeat. Requires the OPERATOR_TOKEN bearer.
- `search_corpus` — Search your tenant's corpus (Matt's posts, videos, repos and AI Hero pages): keyword, semantic, or hybrid (default). Answers the best chunks as bounded snippets with each source's provenance, a permalink, and its resource URI (read it with get_corpus_source). Optional: mode, types, limit (1 to 20). Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `seed_contact_directory` — Seed one resumable page of AI Hero contacts into the contact-directory journey; returns posted and skipped counts plus next, the call that seeds the following page (null when done). Set dryRun to read without touching actors. Only the AI Hero authority tenant (AIHERO_EXECUTOR_TENANT) may call it. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `send_ops_dials` — Operator-only: every dial in the send path with the value in force now, where it comes from, when a change takes effect, and how to change it: a capability and its authority (Knob), the send-ops slice that adds one (Planned), or deploy-time on purpose with the harm a live knob could cause (Deploy). With broadcastId (a sharded parent) it also reads that send's pace, caps and shard count and its readings: the measured rate of recipients the pace counted over the last 10 and 60 minutes, and open flights. A source that can't be read reads unknown with its reason, never a default. nextMoves carries the ready calls. Requires OPERATOR_TOKEN.
- `set_link_target` — Operator-only: hot-swap where sent short links redirect. Give linkId for one link, or messageId + fromTarget to swap every static link of that message, in any version, whose original target is fromTarget. The target must be on the tenant's allowed link hosts. A swap on a personal link overrides every recipient's own target. Audited with its reason. A refusal changes nothing, is audited too, and is a problem: 422 link-target-not-allowed (off the allowed hosts), 404 unknown-link (a linkId not the tenant's), 409 no-matching-links (a bulk form matching no static link). Requires the OPERATOR_TOKEN bearer.
- `set_profile_source` — Operator-only: set where one journey's PostShiba sends get the person from, without a deploy. live (the default with no row) calls ai-hero's personalize endpoint; shadow reads the synced profile, compares, and still sends the live answer; profile sends from the synced profile and calls live only when it can't vouch. Setting live is the kill switch, within 30 seconds. Returns the stored source and the one it replaced. Requires the OPERATOR_TOKEN bearer.
- `set_provider_ramp` — Operator-only: set where one journey's email sends go for a tenant, without a deploy. enabled:false is the kill switch (everything to Kit, within 15 seconds); percent routes new entrants since cohortStart by hash; canaryContacts route whatever the percent. Returns the stored ramp. Requires the OPERATOR_TOKEN bearer.
- `signup_counts` — Read your tenant's daily new-subscriber counts for a range of at most 31 UTC days, from the contact directory's receipts: per UTC day, subscribed (contacts the directory started with contact.created as provisional or confirmed, list imports and directory seeds included, split by the creation event's source), confirmed (moves to confirmed on contact.confirmed) and unsubscribed (moves to unsubscribed on contact.unsubscribed, not split by source). Each day is read on its own; a day that ended before asOf is cached for a few hours, so the first read of a long range is the slow one. A day with no directory evidence is absent, never zero. complete is false for a day still filling. Counts only, never a contact. A wider range is refused. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `actor_log_errors` — Read the actor log's error rows in one window of at most 60 minutes, across every tenant, for the release flag watch. Operator bearer only; a tenant key is refused. Each row carries only its time, event name, Worker version and the flag it named, never a contact or tenant. complete is false when the window holds more than 500 error rows: treat that window as unknown, never clean. A longer or reversed window is refused before any read. Reads the MySQL actor log; nothing is written.
- `trace_contact` — Read one contact's bounded projection timeline: facts, folds, dispatch attempts, receipts, alarms, and wakes. Requires contact and journey; optional since, cursor, and confirmActor. Actor confirmation reads state, wakes, and retries but never redrives. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `trace_event` — Trace one exact event key within a tenant, contact and journey. Reads the retained EventLog, recent stored forward failures and folds, and receipts carrying the same event key. Forward attempts are partial evidence: successful forwards, internal retries and failed evidence writes are not a complete ledger. Missing rows never prove no attempt or fold occurred. Actor qualifiers are required to execute the read; resolve a known email with resolve_contacts first. No replay, send, provider call or storage write. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `validate_message_file` — Check one message file exactly as a push to your content repo would, without pushing: valid, cataloged (a broadcast or archived file, not published by the push; call publish_content separately), or refused with the push's own reasons. Pass the path (messages/<id>.md) and the file's text. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `add_broadcast_recipients` — Continue an enabled tenant's capture using the unchanged draft and selector with the status captureCursor. Disabled tenants retain empty-census dry-run behavior. Tenant/Cohort changes are refused; Contacts pages may add ids until a final marker with complete=true and expectedCount. Schedule requires that reconciled marker. Omitted audience retains any stored binding without another admission read. Supplying audience requires OPERATOR_TOKEN and must match the bound artifact and compiler identity; no legacy capture can acquire an attachment midway through.
- `broadcast_recipients` — Read one broadcast actor's recipient rows by census position, a page at a time from the actor's own storage: each row's outcome, settle time, content revision, the provider's message id and the send key the provider echoes back. Rows carry contact ids, never addresses. Pass next back as from until next is null; a page counts every position it covered even when status narrows the rows returned. For a sharded broadcast, page each shard by the broadcastId sharded_broadcast_status lists. Refused while the actor is freezing its census.
- `broadcast_status` — Read actor-authoritative broadcast status, capture progress, version mix and reconciliation holds; unknown outcomes retain a null ETA.
- `cancel_broadcast` — Cancel a broadcast, stopping future delivery while preserving unknown outcomes.
- `control_sharded_broadcast` — Apply one move to every shard of a sharded broadcast and report each shard's result: continue_capture advances every incomplete legacy shard one page from its own cursor (self-driving shards capture on their own alarms and never need it), schedule, reschedule with a new startAt (a timestamp or "now") for scheduled shards, pause, resume, cancel, edit_pace with the total pace (each shard gets floor(total / count)), edit_caps with the total caps (each shard gets floor(total / count), each refusing below what it already sent), edit_in_flight with the sends in flight per shard (every shard gets the same bound, still capped by the flag rung), or steer with steerPerHour null to clear the slow-only steer the send-lanes controller set on every shard that holds one. Shards for which the move is not valid in their state are skipped, so repeating a move converges. cancel stops future delivery on every shard.
- `create_broadcast` — Create a broadcast pinned to an exact published message revision. Enabled tenants capture a consented Tenant, Contacts or deterministic percentage Cohort census; others accept only empty Contacts dry runs. Campaign id scopes launch-only unsubscribe across broadcasts. Tenant and Cohort censuses capture themselves page by page on the actor's alarm, retrying a failed page read with backoff; read phase and captured in broadcast_status. With startAt (a timestamp or "now") the broadcast schedules itself when capture completes, starting at the later of startAt and completion; without it, call schedule_broadcast once phase is captured. Pause and resume hold and release a self-driving capture; cancel stops it. Contacts appends pages of at most 64 ids and finishes with complete=true and expectedCount equal to captured unique recipients. Existing identities cannot be recreated. Optional audience sits beside selector: an operator-attested compiled artifact, requiring OPERATOR_TOKEN and the tenant audience rung; off or unreadable admission refuses before actor writes. The first page binds its version and compiler identity atomically; existing captures cannot be rebound. A tenant key may create a live broadcast for a campaign drovr registers for its tenant, with draft.approval naming who approved this exact send and where; the approval is stored once on the snapshot, on every shard, and never overwritten. Every other mutation needs a tenant owner or the operator.
- `create_planned_broadcast` — Operator-only: create a broadcast, single or sharded (shards.count 1 to 32), that captures its list just in time and sends with no further call. The plan names captureAt, startAt, startGraceMs, a CampaignCohort selector with a ceiling at or before captureAt, a size band, the pinned published version, and an approval naming the plan's hash (every plan field except the approval). Create writes the plan to every shard, arms the send watch and reads no recipients. Each shard stays a draft until release. Capture starts only after a code check passes at or after captureAt, and reads the cohort's members at or below the ceiling. Release needs a fresh passing check, startAt reached and startAt + startGraceMs not passed; past it the send holds. Before release, add, continue, schedule, reschedule and content edits refuse: change the plan with edit_broadcast_plan. Pause, resume and cancel always work. Refused while the tenant's broadcast-scheduled-launch rung is off or unreadable, and unless the live deployment is one version, the caller's own. Calling again with the same input converges.
- `create_sharded_broadcast` — Create one send split across count BroadcastActors (1 to 32) with ids <broadcastId>-s00 and up. Each shard captures the Tenant or Cohort selector's recipients whose stable digest of tenant, campaign id (or broadcast id) and contact falls in its slot, so the shards partition the census exactly. The pace and caps given are totals: each shard gets floor(total / count). Calling again with the same input converges: existing matching shards are left alone and missing ones are created. Results name each shard's outcome. Every shard captures its slice itself on its own alarm; with startAt (a timestamp or "now") every shard also schedules itself when its capture completes, starting at the later of startAt and that completion, so the send needs no further call and continue_capture is only for legacy shards. Without startAt, schedule with control_sharded_broadcast once phase is captured. A retry must repeat the same startAt. Optional audience requires OPERATOR_TOKEN. Admission is read once before fanout, then the same compiled artifact and compiler attestation bind every shard; off or unreadable admission refuses before any shard writes. Different artifact or compiler identity conflicts; identical retries preserve original recording times. A tenant key may create a live broadcast for a campaign drovr registers for its tenant, with draft.approval naming who approved this exact send and where; the approval is stored once on the snapshot, on every shard, and never overwritten. Every other mutation needs a tenant owner or the operator.
- `dry_run_broadcast_audience` — Operator-only: simulate the supplied ownership-only audience artifact over source/gate pages. Requires OPERATOR_TOKEN, no actor binding, slots or audience flag climb. Writes nothing and creates no broadcast. The caller chooses positive pagesPerCall and continues with the returned opaque cursor. Histograms count evaluated contacts per page, not unique people across pages/calls. Typed unreadable facts and artifact drift keep allowed contacts and name the skipped/refused rule; facts-service defects fail the simulation. Real-reader creation remains separately gated by artifact admission.
- `edit_broadcast_caps` — Change the broadcast's hourly and daily caps in any non-terminal state; the next reservation plans within them. Caps can rise freely and fall only to the most this broadcast already sent in any rolling hour or day of its kept buckets (24 h), an open reservation's take included; below that refuses caps-below-sent with the floor. Slow a live send with edit_broadcast_pace.
- `edit_broadcast_content` — Pin an exact valid published revision for this broadcast's unsent recipients, leaving contact standing pins untouched.
- `edit_broadcast_in_flight` — Hold this broadcast below the platform's sends in flight per shard: each delivery window posts at most the lower of sendsInFlight and the broadcast-shard-send-concurrency flag's rung, so it can only slow the broadcast, never raise it past the flag. A value at or above the rung leaves the flag governing. Open sends finish first; the next window uses the new bound.
- `edit_broadcast_pace` — Edit the broadcast's rate or target window without rewriting an open reservation.
- `edit_broadcast_plan` — Operator-only: replace a planned broadcast's plan on every shard as revision targetRevision. captureAt, the selector, its ceiling and the band change only before any shard has committed a page; startAt, startGraceMs, the pin and the approval change until release, including from a start-late hold. Every shard is fenced before any changes, and no shard releases while the edit is partial. Repeating the call with the answer's editId finishes an interrupted edit; once a shard has applied it, the edit can only be finished. A changed plan needs an approval naming its new hash.
- `edit_broadcast_pool` — Choose the provider's sending IP pool for this broadcast's next sends, or null for the provider's default pool. The pool must be one the provider's pool list shows usable for broadcast sends, or it refuses ip-pool-unavailable with the reason. A pool can change while sending; with no send-error limit set the answer warns, because a pool the provider stops accepting fails every remaining send with nothing to pause it. Desk authority.
- `edit_broadcast_send_errors` — Set the send-error circuit breaker: the broadcast pauses itself when this many consecutive recipients are refused by the provider after posting (a sent recipient resets the run; provider suppression and failures before the post never count), or null to turn it off (the default). The pause commits with the refusal that trips it, sendErrors on the snapshot names the count and reason, and resume_broadcast clears the run. Desk authority.
- `pause_broadcast` — Pause a sending broadcast without discarding an unconfirmed flight.
- `reschedule_broadcast` — Move a scheduled broadcast to a new start time without changing its frozen recipients. Use "now" to start on its next alarm; a past timestamp means now. Refused outside scheduled.
- `resume_broadcast` — Resume a paused broadcast; the recipient gate still guards any replay.
- `retry_uncertain` — Re-check every parked uncertain recipient now, in any broadcast state, completed included. Each is first looked up at the provider by its original send key: delivered settles it sent, dropped settles it skipped, and only a settled missing inside the provider's replay window re-sends it under the same key. Rows parked before this loop existed are enrolled too. The actor works through its rows on its alarm; read broadcast_status (parked, and the uncertain count) to follow it. Nothing is re-sent to a recipient the provider has.
- `schedule_broadcast` — Schedule a broadcast after recipient capture completes.
- `sharded_broadcast_status` — Read a sharded broadcast by its parent id: the rolled-up phase (the least advanced shard's, incomplete while a shard is missing, unreadable or foreign), summed sent, skipped by reason, uncertain and unknown counts, each shard's phase, remaining and capture cursor, the total pace in force and the latest shard ETA (null while any active shard's ETA is unknown). Missing shards show phase missing and offer create_sharded_broadcast. holes lists every shard that will not deliver its slice (cancelled, missing, unreadable or foreign) while any shard is still open; schedule stays closed while any shard is cancelled or still capturing.
- `campaign_cohort_status` — Read a campaign cohort's latest seed or grow run: phase, scanned, eligible, eligibleMembers, added, target, members and digest. run is null before the first run.
- `export_campaign_cohort` — Read one page (at most 1000 members, in cohort order) of a campaign cohort as Kit subscriber import CSV, with your own key; nothing is published at a URL. Pass cursor back until it is null and concatenate the pages. joinedAfter filters in storage before the page limit, keeping only members that joined strictly after that instant. Keep the same joinedAfter when continuing with a cursor. Members without an address are counted in missingEmail and left out.
- `grow_campaign_cohort` — Start growing a campaign's stored cohort to a percentage of the contacts the broadcast send gate allows (consent, suppressions and stops, every gate refusal). Every member stays; eligible non-members join in ascending sha256(seed:contactId) until allowed members reach ceil(percent% of allowed). A second run at the same percent adds nothing while eligibility is unchanged. Runs in a Durable Object on alarms over the whole directory; read campaign_cohort_status for eligible, eligibleMembers, added, target and the member digest.
- `materialize_interest_cohort` — Score every contact with a signal for the source campaign (sources.campaignId) and add those the send gate allows, who are not excluded buyers and score at least threshold, to a separate stored cohort (campaignId). Signals are read where they live: unconfirmed clicks on sources.messageIds (role from sources.linkRoles) and sources.receiptSignals. The policy holds the weights, caps, half-life and excluded products as data; every excluded product needs a buyers-of deny rule on the source campaign. Members never move, so use a new campaignId per cutoff. dryRun writes nothing: read campaign_cohort_status for distribution (score bands, buyers, and the cohort size at threshold and each extra cutoff), gateRefused, and added, members and the digest of the cohort it would leave. Select the cohort in create_broadcast with the CampaignCohort selector.
- `record_interest_facts` — Record up to 500 campaign interest facts that happened outside drovr (Kit clicks and opens, AI Hero checkouts), keyed by drovr contact id. They are append-only and deduplicated by source id within the campaign, and materialize_interest_cohort reads them as signals of their kind and role, beside drovr's own clicks and receipts. Nothing is sent and no contact changes. A tenant key at owner or operator role records facts for its own tenant; seeding, growing and materializing a cohort stay owner-only.
- `seed_campaign_cohort_from_broadcast` — Start adding every contact that holds a drovr link recipient row for one published revision to a campaign's stored cohort. Members never move: a rerun adds nothing. Runs in a Durable Object on alarms; read campaign_cohort_status until phase is done. dryRun counts and digests without writing.
- `list_recipient_rules` — Operator-only: list up to 500 of a campaign's recipient rules (per-person overrides and buyer refusals) with who set them and when. Buyer refusals come first, so the cap never hides one; then per-person overrides. truncated is true when the cap is reached; more per-person rules may exist. Requires the OPERATOR_TOKEN bearer.
- `remove_recipient_rule` — Operator-only: remove one per-campaign recipient rule by tenant, campaign and subject. Removing a missing rule answers removed: null. Requires the OPERATOR_TOKEN bearer.
- `set_recipient_rule` — Operator-only: set one per-campaign recipient rule, without a deploy. A Contact rule is a per-person override: deny excludes them from the campaign; allow lifts policy refusals (already-bought, any campaign filter) but never a consent stop (unsubscribed, suppressed, bounced, complained, unconsented). A BuyersOf rule (deny only) refuses everyone who bought that product. The broadcast send gate and cohort eligibility read these rules at each check. For a Contact rule, effective shows what the gate answers for that person now. Requires the OPERATOR_TOKEN bearer.
- `list_kit_stop_tags` — Operator-only: list at most 500 stored tag-scope rows with who set them and when, grouped into tags, and their admission mapping (source built-in while none is stored). truncated is true when the cap is reached: tags or scopes may be missing, including scopes of the last tag, so admitted is not a complete inventory. Requires the OPERATOR_TOKEN bearer.
- `set_kit_stop_tag` — Operator-only: set which suppression scopes a Kit subscriber.tag_added for one tag writes, without a deploy. The webhook admits only stored tags and records each admitted event's scopes; the Kit fold writes one suppression per scope, and only scope all also stops the contact directory. An empty scopes list removes the tag. Kit's webhook for the tag is registered separately, in Kit. Answers the tenant's admitted tags after the change. Requires the OPERATOR_TOKEN bearer.
- `sync_provider_suppressions` — Operator-only: mirror the email provider's suppression list into drovr all stops for one tenant. Reads every page of the provider's list first; a failed page writes nothing. Matches each address to a drovr contact by exact address, then writes an all stop (bounce as bounced, spam report as complained, unsubscribe and manual as unsubscribed, an unknown reason as unsubscribed and reported by name) for each matched contact without an active one, with a dated mirror source (see source). Never lifts a stop. A drovr resubscribe newer than the provider's unsubscribe stands; a bounce or complaint re-applies, as every stop write does. Writes nothing to Kit or the provider. Each write is read back. Answers counts only, never an address; a second run inserts 0. dryRun:true counts without writing (agent authority); dryRun:false writes (desk authority). Refuses a tenant the provider is not configured for. Requires the OPERATOR_TOKEN bearer.
- `import_list` — Operator-only: write one page of a planned list import into a tenant: the one writer for every list import. Plan the whole capture with @drovr/list-import (planListImport, then importManifest); the runId is the SHA-256 of manifestText. Send the manifest's items in order, at most 25 per call, from cursor 0, following the cursor each page answers. Stops come first: every source opt-out becomes an all stop (unsubscribed; bounced, with a source suppression; or complained, the strongest reason winning) before any consent is recorded, and a page that skips the cursor or puts a person among the stops is refused. An applied people page is refused until every stop item is written (stopsWrittenThrough reaches the manifest's stop count); a stop held as a conflict holds the people pages until it is resolved. Each item names at most 16 source contact ids. Each item matches a contact by its bound aliases and then by exact address, and seeds a new contact when none exists. It never merges contact ids: source ids or an address belonging to different contacts is a conflict, held and not written. A person with an active all stop stays stopped. A mailable person gets an imported consent for the form, guarded by the stop in the same transaction; a quarantined person is imported without consent. A contact the contact directory has not born yet is born into it (so a retry after a failed birth finishes it), and the import never starts a course journey such as value-path (journeys.courseStarts is always 0), where a double opt-in confirmation does. A stop write never lifts a stop; a drovr resubscribe newer than the source's asOf stands and reads as skipped-known. Every write is keyed by tenant, alias, contact, form and runId, so a repeated page writes nothing new. dryRun defaults true and writes nothing. Answers outcomes and contact ids, never an address. Requires the OPERATOR_TOKEN bearer.
- `fill_identity_addresses` — Operator-only: fill the normalized address digest on a tenant's contact identities that have none, so the address index is complete. A bulk writer that inserts identities without the digest leaves the index incomplete, and exact-address reads refuse or hold until it is filled. Each call reads one page of up to 1,000 identities with no digest after the cursor and, on an apply, fills that page with one update under the tenant's identity lock, only where the digest is still empty and the address is unchanged; follow after until exhausted. Changes no address, contact, consent or stop. Answers counts and a cursor, never an address; a second run finds 0. dryRun:true counts without writing (agent authority); dryRun:false fills (desk authority). Requires the OPERATOR_TOKEN bearer.
- `send_health` — Read one send's health from complete sources, section by section: coverage of its shards, settled and uncertain recipients, throughput against the pace (at pace, below pace or idle, as a fact with no cause), stalled shards, the tenant's delivery events over the last 15 minutes, and the tenant's spam complaints against deliveries over the last 24 hours as the provider counts them. Every section is known or unknown with its reason, and an unknown section is never a zero; provider latency, 429s, the governor, pool failures, resets, the events reconcile and stops read unknown until their sources exist. Duplicates count this send's keys processed in the last 8 minutes that hold two provider message ids in one source, naming their shards. A sharded send's parent read without sharded is refused with a pointer to sharded: true. Any tenant role may read.
- `triage_send` — Run one send's health through the send-ops rule catalog: each rule's finding with its harm, state (firing, quiet or unknown, never quiet when its section or its bar's flag is unknown), attention (page, watch or none), the numbers it read, its bar (the value in force and how it moves: a flag through flip_feature_flag, deploy-time with a reason, or planned until a source exists; send_ops_dials lists them as bar-<code> dials) and the ruling it comes from, and the ready calls that act on it with the authority each needs. Findings group by fault cluster; attention is the worst firing one. It never acts: every move is a separate capability call.
- `reconcile_broadcast` — Reconcile one finished send, recipient by recipient, across three sides joined on the send key: what the shard recorded, what drovr composed (its link rows) and what PostShiba processed and delivered (stored delivery events). Each recipient gets one class: ok, sentNoRecord, multiProcessed, procNoSend, crossVersion, stranded, linkNoProc, noLinkRows, skipped or unknown. An absence is judged only once the events feed is complete past that recipient's send plus a 420 s overlap; until then it reads unknown, never ok. Each suspect (sent with nothing processed, or processed with no later event) gets one confirming lookup of that recipient's PostShiba message events, one at a time per tenant and at most 30 a minute; a failed lookup reads unknown, and a held one stops the call at that page. Reads a few pages per call from a caller-held cursor: pass next back until it is null. A shard is read only once it is completed or cancelled. It never resends and offers no resend move; a loss or duplicate is evidence for the desk. Any tenant role may read.
- `send_status` — Read a phone-sized tenant send status: watched in-flight, overdue scheduled and next-72-hour broadcasts, recently finished watches, trailing-60-minute delivery and journey dispatch refusals, plus planned: the planned sends in tenant settings, each with slug, title, startAt and state "not created yet" (a broadcast appears about 70 minutes before its start), "created" (broadcastIds names the watched broadcasts whose id is the slug or starts with slug-) or "overdue" (past startAt with none created); a send drops out 24 hours after its start, and the summary leads with the next one. Input is flat: {"tenantId":"org-example","at":"2026-10-30T18:00:00.000Z"}; omit at for now. Actor snapshots are live, not historical. Every section is known or unknown with a reason; unreadable phases are unclassified, health uncertainty stays with a known phase, no watch means invisible, and truncated tenant inventory is unknown. Includes a plain summary and exact next-call arguments. Read-only; never sends or scans actor logs.
- `deliverability_health` — Read a tenant's deliverability in one call, from Tatami and PostShiba together: tenant totals, each sending IP and each receiving provider over the last hour and 24 hours (sent, delivered, bounced, deferrals, spam reports, traps, with rates derived from those counts), each IP's top receiver replies over the hour, redacted, with the blocklists they name, the mail that ended with no sending IP (expired after retries, or MX-unresolvable) as its own line, Tatami's alert rules (each rule's name, type, severity, state and what it matches; where Tatami sends its alerts is unset and unreadable), and PostShiba's 24-hour spam reports, its queue, its live standing (account suspended, KYC, clusters at cap, and whether this tenant is held), and its live IP routing (the sending cluster's default-pool and pool IPs, each sending IP joined to it by address). Input is flat: {"tenantId":"org-example","at":"2026-11-02T01:00:00.000Z"}; omit at for now. Every section is known or unknown with its source and reason; unknown is never zero, and a Tatami zero that PostShiba's own delivered count contradicts reads unknown (tatami_tenant_mismatch). Per-IP alerts, alert rules, clusters at cap, suspension, KYC and routing are shared, never the tenant's alone; alert rules, standing and routing are read live, even when at is earlier. Read-only; never writes to Tatami or PostShiba.
- `campaign_report` — Read one campaign's send report in one call: sends (each watched sharded broadcast whose draft campaign is campaignId: phase, startAt, captured, sent, remaining, skips and capture exclusions by reason, ETA and uncertain), messages (per-message recipients, clicked and clicked by link role from the engagement rollup's last snapshot, never a live read; stale past 60 minutes), buyers (the products the campaign's buyers-of rules name, and how many contacts bought them) and the tenant's last-hour delivery (sent, delivered, deferred, bounced, complaints and Tatami trap hits). Input is flat: {"tenantId":"org-example","campaignId":"aih-c5-launch","interestCohortId":"aih-c5-lastday-live"}; interestCohortId is optional and only the messages section needs it. Every section reads at once inside one answer budget; a slow or missing source reads unknown with its reason, and the call still answers. Includes a plain summary and exact next-call arguments. Read-only; never sends, never starts a rollup.
- `broadcast_engagement` — Read how up to 50 broadcasts landed, counts only, never a contact or an address: mail (actor-reported sent, captured, remaining), delivery (delivered, hard and soft bounces, complaints), opens (distinct contacts by open class, with people, machineOnly and any), clicks (distinct person clickers, person clicks, and per link by host and path), unsubscribes, and rates (open, any-open, click and click-to-open, over delivered or sent). Single and sharded broadcasts are both accepted by id; a sharded broadcast sums every shard or reads unknown when one is unreadable. Opens and clicks are counted per message version, so a version two broadcasts sent counts in both; then the rates divide by the version's registered contacts (basis registered). A broadcast that sent nothing reads opens and clicks unknown (not_sent). Each section carries asOf and source; a section no stored fact answers reads unknown with its reason, never zero. Input: {"tenantId":"org-example","broadcastIds":["aihero-c5-04-fri"]}. Read-only.
- `watch_send` — Arm or disarm the send watch on one send, by hand. create_broadcast, create_sharded_broadcast and schedule_broadcast arm it as they answer; use this when their answer says unarmed, to set per-watch bar overrides, or to stop watching. arm reads the send first and refuses one that doesn't exist. The watch only reads and alarms: it never pauses, cancels, edits or resends. Owner only.
- `send_watch` — Read the send watch for one tenant: its alarm rows after a cursor, oldest first, and its open watches with the rules that read firing or unknown now. Each alarm is one rule changing state on one watch, with its attention: page, watch or record. Pass cursor back as after to read only what is new; a relay delivers each alarm id once. The watch only reads and alarms: it never pauses, cancels, edits or resends. The ledger is MySQL only, so a D1 stage answers unavailable. Any tenant role may read.
- `monitor_alarms` — Operator-only: read the Axiom monitor alarms drovr recorded, oldest first, after a cursor. Each row is one notification from Axiom's drovr-alarm webhook: a monitor opening or closing, with its level, value and group. Pass cursor back as after to read only what is new; handle each id once. Recording only alarms: it never brakes, holds or sends. MySQL only, so a D1 stage answers unavailable. Requires OPERATOR_TOKEN.
- `get_cta_policy` — Read the tenant's next-best-CTA policy (offer ladder, placements, holdout, interest weights) and its version. A tenant with none answers version 0 and a null policy. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_next_cta` — Read the one next-best CTA for a contact at a placement: an offer id the site renders with its own copy, the reason it was chosen (pitch, or why the placement default instead: unknown-contact, holdout, not-ready, ownership-unknown, not-in-placement, ladder-complete), the score band, an optional theme (the contact's leading topic, soft) and the policy revision. Never blank once served. drovr returns ids, never copy. Off per tenant behind the next-cta flag; the policy comes from set_cta_policy. The tenant comes from authentication, never the input. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `set_cta_policy` — Replace the tenant's next-best-CTA policy. Owner only. A compare-and-set: expectedVersion names the version it replaces (get_cta_policy reads it), and a stale one is refused with the stored version. Refused when the policy names another tenant. Stores ids and numbers only, never copy. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `get_contact_segments` — Read a contact's segments rebuilt from original answer facts, with the policy revision and each answer's source, timestamp and fact reference. A stated answer beats every inferred answer; within a source the latest answer wins. Unanswered dimensions are listed as unknownDimensions, never invented. Tenant comes from authentication. Nothing sends or changes a contact. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `segment_coverage` — Operator read of segmentation coverage over the tenant's contactable list: identified, consented contacts without an active all-list suppression. Per dimension, count and share for every configured value plus unknown, with stated and inferred counts separate. Rebuilt from paged facts and current contactability; startedAt and completedAt bound the live traversal, not an atomic historical snapshot. pagesRead, factsRead and durationMs expose its actual work. A traversal budget exhaustion refuses the read; no partial census is returned. Requires the operator bearer, not a tenant key. Nothing sends. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `preview_cut_facts` — Operator read of the facts a C5 audience cut would see for one capture page. It batch-reads Kit open-history windows, interest-fact scopes, segment answers, receipt causes and product ownership per contact, and with engagement, owned engagement windows. Each fact is present, absent or unknown with a cause. Kit absence is known only under a supplied complete coverage record for this tenant and scope; without one it is uncovered. A contact this tenant does not hold is not-in-tenant, and nothing is read for it. A failed source answers unknown, never zero. Counts come first; includeContacts adds each contact's facts and source refs. Nothing is written or sent. Requires the operator bearer, not a tenant key. Authorization: Bearer <api key> — per-tenant keys, operator-minted.
- `backfill_contact_identities` — Operator-only: repair confirmed, unheld profiles missing a contact identity, currently for the configured AI Hero tenant. Resolve addresses server-side from AI Hero AI_Contact by contactId, checking its associated AI_User email. dryRun defaults true and writes nothing. Apply inserts only missing identities with firstName null, profileVersion 0 and normalized address digest in the same contact-locked transaction, under the tenant identity lock, then reads back. Never merges or changes existing identities, consent, stops or provider accounts. Conflicting ownership, duplicate source addresses, user mismatch and unresolvable addresses are counted, not written. Requires a complete address index; fill_identity_addresses if incomplete. Each call reads at most limit candidates after cursor; follow cursor until exhausted. Counts and cursor only, never addresses. Requires OPERATOR_TOKEN. An apply is a production identity write and needs operator approval.

Every MCP request sends `Accept: application/json, text/event-stream`, the
`MCP-Protocol-Version: 2026-07-28` header, and request
`_meta` carrying `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities`;
the server card is `/.well-known/mcp.json`. Resources: `drovr://guide` (this
guide), `drovr://journeys/{id}/v{version}` (each served release), and
`drovr://tenants/{tenant}/contacts/{contact}/journeys/{journey}` plus
`/trace` (your own tenant's contacts),
`drovr://tenants/{tenant}/corpus/sources/{id}` (one corpus source),
`drovr://tenants/{tenant}/content/messages/{id}` (one content message) and
`drovr://tenants/{tenant}/content/pushes/{commit}` (what one push did:
re-read it after you push; there are no subscriptions). Their ids complete.
Prompts, read with any valid API
key: `draft-broadcast`, `email-sequence-architect`, `kit-automation-architect`, `survey-segmentation-engine`: methods for
designing email journeys with these tools.

Code mode: `search` finds capabilities and returns their TypeScript
signatures; `execute` runs one JavaScript program (the body of an async
function) in a sandbox with no network, where
`tools.campaign_engagement(input)`, `tools.conversion_funnel(input)`, `tools.describe_surface(input)`, `tools.dry_run_product_interest(input)`, `tools.explain_send_path(input)`, `tools.get_audience_total(input)`, `tools.get_contact(input)`, `tools.get_contact_list_state(input)`, `tools.get_contacts(input)`, `tools.get_content_message(input)`, `tools.get_content_push(input)`, `tools.get_corpus_push(input)`, `tools.get_corpus_source(input)`, `tools.get_email_delivery(input)`, `tools.get_feature_flag(input)`, `tools.get_kit_subscriber_stops(input)`, `tools.get_receipts(input)`, `tools.get_review(input)`, `tools.get_status(input)`, `tools.get_vitals(input)`, `tools.get_wakes(input)`, `tools.inspect_metric(input)`, `tools.journey_funnel(input)`, `tools.journey_health(input)`, `tools.list_births(input)`, `tools.list_content_messages(input)`, `tools.list_content_publishes(input)`, `tools.list_delivery_events(input)`, `tools.list_feature_flags(input)`, `tools.list_pauses(input)`, `tools.list_stranded_intents(input)`, `tools.list_suppressions(input)`, `tools.new_message(input)`, `tools.render_content_version(input)`, `tools.request_traffic(input)`, `tools.resolve_contacts(input)`, `tools.resolve_directory_identities(input)`, `tools.search_corpus(input)`, `tools.signup_counts(input)`, `tools.trace_contact(input)`, `tools.trace_event(input)`, `tools.validate_message_file(input)`, `tools.campaign_cohort_status(input)`, `tools.export_campaign_cohort(input)`, `tools.get_cta_policy(input)`, `tools.get_next_cta(input)`, `tools.get_contact_segments(input)`
call the read-only capabilities with your own key and `return` hands a
JSON value back. `execute_writes` also reaches
`tools.post_event(input)`, `tools.post_event_batch(input)`, `tools.record_signup(input)`, `tools.redrive_contact(input)`, `tools.release_journey(input)`, `tools.seed_contact_directory(input)`:
each write asks you to approve it through MCP form elicitation (one round
per write, the program re-running each round with its randomness and clock
replayed), at most 5 per program; a client without elicitation is refused.

## Errors

Refusals are RFC 9457 problem details (`application/problem+json`):
`type` is a stable `urn:drovr:problem:<slug>` identifier, `detail`
says what happened, `hint` says how to fix it, and the valid branches
ride beside them (`routes`, `journeys`, `example`). The problems:

- `actor-history-without-snapshot` — Actor state lost. Do not retry: this contact's actor holds history but no snapshot, and it refuses every event until an operator inspects it. Trace the contact with GET /contacts/trace (MCP trace_contact) and report the contact and journey to the drovr operator.
- `actor-log-errors-unavailable` — Actor log errors unavailable. The actor log store didn't answer the window's read (the detail says why). The read changes nothing: repeat it after a short wait. A flag watch treats this as an unknown window, never a clean one.
- `actor-log-errors-window-refused` — Actor log errors window refused. Pass from and through as epoch milliseconds, through after from and at most 60 minutes later; read a longer span as several windows.
- `actor-refused` — Actor refused the event. Retry is safe: delivery is at-least-once and the fold dedupes on idempotencyKey.
- `actor-rejected` — Actor rejected the event. Do not retry this event unchanged: the actor refused it for the reason in detail. Fix the event, or trace it with GET /contacts/trace (MCP trace_contact).
- `api-key-required` — The tenant's API key is required. This capability is a source system's own report: call it with the tenant's API key, not a person's grant.
- `api-key-store-unavailable` — API key could not be checked. The key store could not be read, so this key was neither accepted nor refused. Wait the Retry-After seconds and send the same request again; nothing was refused. Retry is safe: delivery is at-least-once and the fold dedupes on idempotencyKey.
- `audience-total-unavailable` — Audience total unavailable. Retry get_audience_total (GET /audience-total). No completed census is readable; do not substitute zero. The operator can inspect audience-total-refresh and its ledger to restore refresh.
- `axiom-webhook-unavailable` — Axiom alarm webhook unavailable. Axiom retries a failed notification. Configure AXIOM_ALARM_WEBHOOK_SECRET on DrovrApi (the bearer drovr-alarm sends) on a MySQL deployment; nothing is recorded unverified.
- `births-request-refused` — Births request refused. Pass journeyId alone to read from the beginning, since alone to start the first page at an instant, or cursor alone (nextCursor or resumeCursor, unchanged) to continue. limit is a whole number from 1 to 1000.
- `births-unavailable` — Births feed unavailable. D1 didn't answer the receipt projection, or it is shedding load and operator routes are paused, so no page was read; the detail says which. Wait the Retry-After seconds and repeat the same request: reads change nothing and your cursor stays valid.
- `broadcast-broadcast` — Broadcast address refused. Resolve the actor by the exact tenant and broadcast address.
- `broadcast-already-initialized` — Broadcast already initialized. This identity already has a broadcast. Read its actual status, or use a different identity for a new draft or selector.
- `broadcast-approval-required` — Broadcast approval required. A tenant key may create a live broadcast only with draft.approval: who approved this exact send (by) and where that approval is recorded (ref). Record the approval, then create the broadcast again with it. Every other broadcast mutation stays with a tenant owner or the operator.
- `broadcast-audience-dry-run-unavailable` — Audience dry run unavailable. Retry dry_run_broadcast_audience after its source or gate becomes readable, preserving the opaque cursor. This read-only simulation created no actor, allocated no slots and wrote nothing.
- `broadcast-audience-off` — Audience attachment not admitted. The broadcast-audience-filter rung is off for this tenant. Omit audience to create without narrowing, or inspect get_feature_flag and retry with audience only after an authorized rung admits it. No actor or shard was written by this request.
- `broadcast-audience-operator-required` — Audience operation requires operator authority. Use OPERATOR_TOKEN authorization to record a compiled audience or call dry_run_broadcast_audience. Omit audience for ordinary tenant-owner creation. No actor or shard was written by this request.
- `broadcast-audience-unavailable` — Audience admission temporarily unavailable. Audience admission could not be read. Retry this create, or omit audience to create without narrowing; inspect get_feature_flag for this tenant. An off rung never admits an attachment. No actor or shard was written by this request.
- `broadcast-batch` — Broadcast batch refused. Read the actor's status; do not manufacture delivery acknowledgements.
- `broadcast-not-planned` — Broadcast has no plan. edit_broadcast_plan edits only a broadcast created by create_planned_broadcast. Read broadcast_status for the controls this broadcast takes, or create a planned broadcast. Nothing was written.
- `broadcast-planned-operator-required` — Planned broadcast requires operator authority. create_planned_broadcast and edit_broadcast_plan take OPERATOR_TOKEN authorization; a tenant key reaches neither. Nothing was written.
- `broadcast-scheduled-launch-mixed-version` — Planned broadcast admission refused. A plan is admitted only when the live API deployment is a single version, the caller's own, whose shards keep a plan in custody until release. This deployment could not prove that, so no plan was created and nothing was written. Retry create_planned_broadcast once the rollout has settled on one version.
- `broadcast-scheduled-launch-off` — Planned broadcasts are off. The broadcast-scheduled-launch rung is off for this tenant, so no plan was created and nothing was written. For a send that captures now, use create_broadcast or create_sharded_broadcast with startAt. Read get_feature_flag, and retry only after an authorized rung admits plans.
- `broadcast-scheduled-launch-unavailable` — Planned broadcast admission unavailable. The broadcast-scheduled-launch flag could not be read for this tenant, and an unread flag never admits a plan. No plan was created and nothing was written. Retry create_planned_broadcast.
- `broadcast-campaign-scope-unmapped` — Campaign opt-out scope has no provider. Each opt-out scope a campaign declares must be proven through a provider the tenant uses, and no provider maps this one, so drovr cannot tell who opted out of it. The tenant's provider settings map each scope the detail names, or the campaign drops the scope; both are reviewed settings changes. Then create the broadcast again. A dry run on a tenant off the live switch is not refused.
- `broadcast-campaign-stop-unproven` — Campaign opt-out scope not proven. This campaign's audience opted out through a provider tag whose drovr scope is not stored yet, so a real send could reach them. An operator imports the tag's members as course:<campaign id> suppressions and reads them back, then calls set_kit_stop_tag for each tag the detail names; list_kit_stop_tags shows what is stored. Then create the broadcast again. A dry run on a tenant off the live switch is not refused.
- `broadcast-campaign-unregistered` — Broadcast campaign not registered for the tenant. A tenant key may create a live broadcast only for a campaign drovr registers for that tenant, so each campaign's opt-out scope is checked before it sends. Use a registered campaign id in draft.campaign, or ask the operator to register the campaign or create the broadcast.
- `broadcast-caps-below-sent` — Broadcast caps below what was already sent. Caps can rise freely and fall only to what this broadcast already sent in a rolling hour or day (the floor is in detail). To slow a live send, lower its pace instead.
- `broadcast-capture-complete` — Recipient capture already complete. The existing recipient census is complete. Read status; a later caller page was not added. Multiple caller pages need a separately approved capture protocol.
- `broadcast-capture-conflict` — Recipient capture input conflicts. Continue the original tenant capture with its unchanged draft fields and opaque cursor, or read status and start a different broadcast. A changed selector or draft was not accepted.
- `broadcast-capture-incomplete` — Recipient capture incomplete. Continue capture with the opaque cursor, including empty eligible pages.
- `broadcast-cursor-refused` — Recipient capture restart required. Do not treat a refused cursor as completion or retry it forever. Create a new capture after inspecting status.
- `broadcast-freezing` — Recipient capture freezing. Read status while the actor freezes the captured recipients.
- `broadcast-invalid-input` — Invalid broadcast input. Use the contract's exact input shape and the last opaque cursor.
- `broadcast-ip-pool-unavailable` — IP pool not usable. Choose a pool the provider's pool list shows usable for broadcast sends (detail names why this one fails; send_ops_dials lists the pools), or null for the default pool. A pool the provider would refuse is never set: pooled mail never falls back to the shared IPs.
- `broadcast-owner-required` — Broadcast owner required. A tenant owner must perform broadcast mutations; viewers and operators may read status.
- `broadcast-pace` — Broadcast pace refused. Choose a positive rate or a target window after the start time.
- `broadcast-sender-unbound` — Tenant has no sender binding. This tenant is on the live broadcast switch but has no sender binding, so its broadcasts cannot send through PostShiba and a schedule would fail when it started. Nothing was scheduled. An operator binds a sender for the tenant (a reviewed settings change), then schedule or reschedule again; send_ops_dials shows the tenant's transport.
- `broadcast-tenant` — Tenant refused. Use your own tenant: a caller reads and changes only its own tenant's records.
- `broadcast-terminal` — Broadcast is terminal. Read the final status; create a different broadcast to send again.
- `broadcast-tick-too-small` — Broadcast tick too small. Use the actor's supported pacing tick.
- `broadcast-time` — Broadcast time refused. Use a start time consistent with the actor clock.
- `broadcast-transition` — Broadcast transition refused. Read status and choose a move valid in that state.
- `broadcast-unavailable` — Broadcast dependency unavailable. Read status before repeating a mutation whose result is unknown.
- `broadcast-uninitialized` — Broadcast not initialized. Create a draft with a published revision and its first recipient page.
- `broadcast-version-invalid` — Broadcast revision invalid. Correct the message's publish checks before pinning a revision.
- `broadcast-version-not-published` — Broadcast revision not published. Choose an existing exact published revision of this tenant's message, not a draft or the latest pointer.
- `broadcast-watch-override-bounds` — Watch override out of bounds. Each override must sit inside its bar flag's catalog bounds, which the detail names; send_ops_dials lists each bar-<code> dial with its unit.
- `campaign-cohort-cursor-refused` — Cohort export cursor refused. Pass the cursor from the previous export page unchanged, or start again without one.
- `campaign-cohort-invalid-input` — Cohort input refused. joinedAfter is an ISO-8601 instant, e.g. 2026-10-22T00:00:00.000Z.
- `campaign-cohort-owner-required` — Cohort owner required. A tenant owner or the operator must seed or grow a cohort; anyone in the tenant may read status.
- `campaign-cohort-running` — Cohort run in progress. Another seed or grow is running for this campaign. Read campaign_cohort_status until its phase is done or failed, then start yours.
- `campaign-cohort-tenant` — Cohort tenant refused. Use your own tenant.
- `campaign-cohort-unavailable` — Cohort dependency unavailable. Read campaign_cohort_status before starting again: the run may have started.
- `campaign-engagement-refused` — Campaign engagement read refused. Pass the source campaign as campaignId and, as interestCohortId, a different cohort whose latest run is a materialize_interest_cohort run over that campaign (campaign_cohort_status shows its job's sources.campaignId). Name each lane once. Nothing was read.
- `campaign-report-invalid-input` — Invalid campaign_report input. Call campaign_report with {"tenantId":"org-example","campaignId":"aih-c5-launch"}, and add "interestCohortId":"aih-c5-lastday-live" for the messages section. Arguments are flat, not nested under address.
- `code-mode-budget-exhausted` — Code mode program budget exhausted. One execute call runs in one invocation, with a budget of 800 database statements and 5,000 subrequests (a write starts only with 100 statements left). Split the work across execute calls and page with the capability's cursor; each call starts a fresh budget.
- `code-mode-state-refused` — Code mode request state refused. Start the program again with a fresh execute_writes call: requestState must come back unchanged, with the same tenant key and the same program, within 10 minutes.
- `code-mode-storage-unavailable` — Code mode storage unavailable. The deployment's storage root refused or could not be acquired, so this call read and wrote nothing; the detail names the reason. A connection failure can clear: start a fresh execute call later. A configuration refusal (invalid-setting, mysql-unbound, mysql-incomplete) needs the operator to fix the Worker's storage settings first; until then call the tools directly and expect the same refusal.
- `code-mode-writes-unavailable` — Code mode writes unavailable. Configure BETTER_AUTH_SECRET, which signs execute_writes' approval state; until then call the write tools directly.
- `cold-start-unhandled` — Event cannot start this journey. Do not retry this event: it is logged, and it folds by itself, in occurredAt order, when the contact starts this journey. The contact has no actor on this journey and this event type does not start one. If the contact should be on the journey, post its start event (for example contact.created) once, then read GET /contacts (MCP get_contact) to see the state with this event folded; if it should already be there, trace it with GET /contacts/trace (MCP trace_contact).
- `confirm-expired` — Confirmation link expired. This confirmation link has expired. Ask for a new one from the page (POST /confirm/resend with the same token); the new email carries a fresh link.
- `confirm-not-enabled` — Confirmation not enabled for this tenant. This tenant has not turned on double opt-in confirmation. Nothing was confirmed; the link stays valid until it expires.
- `confirm-page-unknown` — No confirmation page for this tenant or form. This tenant or form has no confirmation page. Use the link from the email as sent, or contact support.
- `confirm-suppressed` — Address suppressed. This address has unsubscribed or bounced, and a confirmation does not lift that. Nothing was subscribed.
- `confirm-target-not-served` — This form's journey is not served yet. This form's journey is not served on this deployment yet, so nothing was recorded, confirmed or sent. Retry after the deployment that serves it; a confirmation link stays valid until it expires.
- `confirm-unavailable` — Confirmation unavailable. Confirmation is not configured on this deployment yet. The link stays valid until it expires; retry later or contact support.
- `confirmation-required` — Confirmation required. An agent's call of this tool needs the person it acts for to confirm it, through MCP input-required (elicitation): call from a client that supports it, answer the form, and re-call with the same arguments and the requestState. Or call the REST route with a static key, which is itself the approval.
- `confirmation-resend-refused` — Confirmation resend refused. Use an existing pending Ratstack contact and a stable resendId. No signup, deadline extension or suppression bypass is allowed; budget is one resend per contact per 72 hours.
- `confirmation-reserved` — Confirmation not accepted for this journey. Post contact.confirmed only to contact-directory (a provider's confirmation) or double-opt-in. A journey is born on a confirmation only through drovr's own confirm link (double opt-in, #55); to start a contact on this journey, post its birth event (contact.created).
- `consent-begin-refused` — Consent inbox begin refused. Invoke begin_consent_inbox before frozen mode with the operator bearer, fresh previous cycle counts/revision/generation, a new cycle identity and the Held main epoch generation. Writes and sends must both be Held. Never reset pending facts or release admission.
- `consent-begin-unavailable` — Consent inbox begin unavailable. Restore the native D1 source binding and authoritative epoch/cycle state, then read the cycle before deciding whether to retry. A failed readback does not authorize resetting a committed cycle.
- `consent-drain-refused` — Consent drain refused. Use drain_consent_inbox's declared input with fresh source cycle and main epoch pins, explicit page policy and operator bearer. Only mysql mode with the D1 epoch Held (writes and sends) at the pinned generation permits this drain. A retry never releases sends.
- `consent-drain-unavailable` — Consent drain unavailable. Restore the source/target binding or reconcile the typed storage failure, then retry the same pinned cycle and last acknowledged cursor. A committed target receipt replays before the source mark; no failed page cursor is acknowledged.
- `contact-audit-key-mismatch` — Contact audit names another actor. Do not retry with the same ids: the actor this object holds is not the one the query named. Check tenantId, contactId and journeyId, then call GET /contacts/audit again; trace the contact with GET /contacts/trace if the ids are right.
- `malformed-contact-audit-query` — Malformed contact audit query. Send non-empty tenantId, contactId and journeyId query parameters: GET /contacts/audit?tenantId=&contactId=&journeyId= with the OPERATOR_TOKEN bearer.
- `contact-audit-too-large` — Contact actor too large to audit. The audit never returns part of an actor. The detail gives its stored bytes and keys per prefix. Read the parts that answer the question instead: GET /contacts for state and wakes, GET /contacts/trace for its timeline. Report the actor to the drovr desk: an object this large is anomalous.
- `contact-batch-unavailable` — Contact batch evidence unavailable. Retry resolve_contacts or get_contacts with the same items. Missing evidence is not a missing contact. Email lookup requires MySQL and a complete address index (fill_identity_addresses fills it); Kit aliases require the configured authority tenant and directory source.
- `contact-erased` — Contact cannot accept events. This contact cannot accept events. Do not retry this event.
- `contact-erasure-unavailable` — Erasure admission unavailable. Erasure admission could not be checked. Retry this event.
- `contact-final` — Contact is in a final state. Do not retry this event: the contact's actor is in a final state (the detail names it, for example unsubscribed), its journey version does not handle this event type, and no migration moves a finished contact, so the event never folds. Nothing was recorded. Stop the delivery chain; read GET /contacts (MCP get_contact) to see the contact's state. In /events/batch the item is `rejected`, and its detail carries the actor's answer with code `contact-final`.
- `contact-list-state-alias-forbidden` — Kit subscriber alias not available to this tenant. Ask by contactId instead: only the tenant named by AIHERO_EXECUTOR_TENANT may resolve a Kit subscriber id, because the alias lives in the AI Hero directory.
- `contact-list-state-malformed` — Contact list state needs exactly one contact name. Call get_contact_list_state (GET /contact-list-state) with exactly one of contactId or kitSubscriberId.
- `contact-list-state-unavailable` — Contact list state evidence unavailable. Retry get_contact_list_state with the same arguments. Missing identity, consent or stop evidence is not a list state; treat the contact as unknown and keep asking, never as on the list.
- `contact-never-born` — Contact never started this journey. Do not retry this event: it is logged, and it folds by itself, in occurredAt order, when the contact starts this journey. The contact never started this journey, and this event type does not start it. If the contact should be on the journey, post its start event (for example contact.created) once, then read GET /contacts (MCP get_contact) to see the state with this event folded; otherwise nothing more is needed.
- `contact-not-found` — Contact actor not found. Check the contact and journey ids. If the contact has not started this journey, post its first event.
- `malformed-contact-query` — Malformed contact query. Send both non-empty contact and journey query parameters.
- `contact-read-failure` — Contact read failure. Retry the read. If it still fails, inspect the service logs.
- `contact-read-transient` — Contact call transient failure. The platform failed this call for a moment. Retrying is safe: a read changes nothing, and a redrive is idempotent on the intent key, so a part that already ran runs no second time. Wait the Retry-After seconds and send the same request again.
- `contact-trace` — Contact trace request refused. Send contact and a served journey; since is an ISO instant earlier than now; replay a cursor only with the filters that minted it.
- `content-publish-failure` — Content publish failed. Repeat the same publish; the same content is the same version.
- `content-push-not-wired` — Push-to-publish not wired. Only tenants with a content push subscription (CONTENT_PUSH_TENANTS) may open a content repo; wire the tenant's subscription first.
- `content-push-unavailable` — Content push reprocess unavailable. Reprocessing again is safe: nothing was enqueued unless the answer said so.
- `content-read-failure` — Content read failed. Repeat the read; reads change nothing.
- `content-repo-failure` — Content repo failed. Open the repo again; the repo is created once, and a failed call mints no token you hold.
- `content-repo-unavailable` — Content repos unavailable. This deployment has no content repo namespace bound; open the repo on one that does.
- `conversion-read-too-large` — Conversion read too large. A read stops once its database cost exceeds 40000 rows: D1 counts scanned rows; MySQL counts fetched rows. The budget is checked after each batch, so the batch that passes it has already run and the answer is refused, never truncated. A full 2-day launch window reads about 60,000 D1 rows, so it is refused by design, not by a fault: narrow the window, or read the span as several cohorts. The detail states the database units used.
- `conversion-unavailable` — Conversion read unavailable. D1 didn't answer the receipts or clicks read, or it is shedding load and operator routes are paused, so no read ran; the detail says which. Wait the Retry-After seconds and repeat the read: it changes nothing. GET /status reports the pause while it holds: its last snapshot with paused: true, or a 503 with Retry-After.
- `conversion-window-refused` — Conversion window refused. Pass a cohort window (from, to) of at most 2 days, to after from, and until (if given) not before to and at most 30 days after it. Read a longer span as several cohorts.
- `corpus-unavailable` — Corpus index unavailable. Repeat the read; reads change nothing. A deployment with no corpus index answers this until one is bound.
- `course-already-started` — Course already started. At least one course email reached this learner, or no committed receipt proves it did not, so the course has started and never restarts. Do not retry; trace the contact with GET /contacts/trace (MCP trace_contact) if the history looks wrong.
- `course-restart-bounced` — Course restart refused: bounce or complaint. A bounced or complained address never gets a course restart. Do not retry.
- `course-restart-no-actor` — No course actor to restart. Check tenantId, contactId and journeyId; the contact must have an actor on that journey. Read it with GET /contacts?contact=&journey= (MCP get_contact).
- `course-restart-no-entry` — Course restart refused: journey has no entry. This journey declares no birth event, so a restart cannot choose an entry to restart from. Do not retry; check the journeyId with GET /contacts?contact=&journey= (MCP get_contact), and restart only a journey that declares a birth event.
- `course-restart-not-finished` — Course still running. Only a course that ended in a final state restarts. A running course needs nothing; read its state with GET /contacts?contact=&journey= (MCP get_contact).
- `course-restart-owed` — Course restart waits on owed work. The actor still owes a completion or a birth replay from its last run. Wait for it to settle (MCP list_stranded_intents shows it), then call restart_course again with the same evidence.
- `course-restart-unavailable` — Course restart unavailable. Retry the same call with the same evidence: a restart is keyed by its evidence, so a repeat never restarts twice. dryRun:true checks the guards without writing.
- `course-restart-unsubscribed` — Course restart refused: active unsubscribe. Fresh confirmed sign-up first: the learner's new confirmed sign-up lifts the unsubscribe. Then call restart_course again with the same evidence.
- `cta-policy-refused` — CTA policy refused. The policy was not stored: read the detail. A policy names the caller's tenant, in both its interest policy and its ladder. Nothing changed.
- `cta-policy-stale` — CTA policy changed since you read it. Another write replaced the policy after the version you named. Read it again with get_cta_policy, reapply your change to that version, and send its version as expectedVersion. Nothing changed.
- `cta-policy-unavailable` — CTA policy unavailable. The CTA policy store could not be read or written. Try the same call again; a write that was refused changed nothing, and one that landed answers stale on a retry with the same expectedVersion.
- `deliverability-health-invalid-input` — Invalid deliverability_health input. Call deliverability_health with {"tenantId":"org-example"} or {"tenantId":"org-example","at":"2026-11-02T01:00:00.000Z"}. Arguments are flat, not nested under address.
- `delivery-events-request-refused` — Delivery events request refused. Pass since as an ISO-8601 instant, and until (after since) or leave it out for now. limit is a whole number from 1 to 500. reputationOnly is true or false. Pass cursor only as nextCursor from the prior page, with the same window.
- `delivery-events-unavailable` — Delivery events unavailable. No page was read; the detail says why. When the store did not answer, wait the Retry-After seconds and repeat the same request: reads change nothing and your cursor stays valid. When the deployment does not read MySQL, retrying will not help until it does.
- `directory-identities-tenant-forbidden` — Directory identity lookup tenant forbidden. Only the tenant named by AIHERO_EXECUTOR_TENANT may resolve AI Hero directory identities.
- `directory-seed-tenant-forbidden` — Directory seed tenant forbidden. Only the tenant named by AIHERO_EXECUTOR_TENANT may seed the AI Hero directory.
- `directory-source-unavailable` — Directory source unavailable. Configure the AIHERO_DB Hyperdrive binding before retrying the directory seed.
- `directory-stops-tenant-unknown` — Unknown tenant. drovr has no receipts for this tenant, so it has no contact directories and no HUD hub to read. Check the tenant id; nothing was started.
- `directory-stops-unavailable` — Directory stops unavailable. The HUD hub's actor heads or the suppression rows could not be read; the detail says why. The read changes nothing: repeat it.
- `malformed-email-delivery-query` — Malformed email delivery query. Send contact (a non-empty contact id) and email (one of the value-path emails the detail lists).
- `event-not-live` — Event type not live on this actor yet. Retry this same event later, unchanged: this contact's actor is pinned to an older journey version that does not handle this event type, and nothing was recorded, so the event lands once the actor migrates to the version that does (an operator migration). Do not change its idempotencyKey and do not drop it. In /events/batch the item is `failed` with a detail starting `event-not-live:`.
- `event-redrive-failure` — Event redrive unavailable. The event log, its receipts or the redrive cursor could not be read or written, or D1 is shedding load; the detail says why. Anything already re-forwarded stays folded, and a re-forward is idempotent: repeat the same request later.
- `event-trace-needs-actor` — Event trace needs an actor. Provide tenantId, contactId, journeyId and the exact recorded idempotencyKey. If the contact is known by email, call resolve_contacts first and use its contactId. This read cannot search an arbitrary source id across actors.
- `event-trace-unavailable` — Event trace unavailable. Repeat trace_event with the same actor and key after MySQL is available. Reads change nothing. A deployment without the MySQL binding cannot serve this capability; no event was replayed or forwarded.
- `feature-flag-flip-refused` — Feature flag flip refused. Follow nextMoves: record the current rung's exit evidence with record_flag_evidence, use a credential that covers the rung, or flip off. read_flag_ledger shows what is recorded.
- `feature-flag-flip-unrouted` — Feature flag climb has no delivery route. Set the route first: set_provider_ramp with this tenant and journey, enabled and a percent above 0, then repeat the flip. Off is always allowed.
- `feature-flag-invalid` — Invalid feature flag key. GET /feature-flags lists inspect links with valid public keys.
- `feature-flag-read-failure` — Feature flag read failed. A feature flag this route needs could not be read, so the route refuses rather than guess. Retry shortly.
- `feature-flag-route-unreadable` — Feature flag route unreadable. The climb reads the journey's provider ramp before it moves the flag, and that read failed, so nothing changed. Retry the flip, and page the send desk if the ramp stays unreadable. Off never reads the route.
- `feature-flag-store-unavailable` — Feature flag store unavailable. Flag writes need the stage's FLAGS store, which every stage binds. A deployment without it predates the binding: deploy the current Stack, or keep the legacy Worker setting until then.
- `feature-flag-unknown` — Public feature flag not found. GET /feature-flags lists public catalog entries; private and unknown keys are not disclosed.
- `feature-flag-write-failure` — Feature flag write failed. Nothing was written. Repeat flip_feature_flag; get_feature_flag shows the catalog entry.
- `feature-flag-write-invalid` — Invalid feature flag write. GET /feature-flags/inspect?key= shows the flag's scopeSchema and valueSchema; send a scope and value they accept.
- `flag-ledger-failure` — Flag ledger failed. The flag's ledger did not answer; nothing was recorded for this call. Retry; read_flag_ledger shows what the ledger holds.
- `flag-ledger-unavailable` — Flag ledger unavailable. Flips and evidence need the stage's flag ledger, which every stage binds. A deployment without it predates the binding: deploy the current Stack.
- `front-webhook-unavailable` — Front webhook unavailable. Front rule webhooks are not retried: configure FRONT_RULES_WEBHOOK_SECRET (the Front Webhooks app's API secret) and AIHERO_EXECUTOR_TENANT on DrovrApi, then re-send from Front if the event matters.
- `hold-operation-failure` — Journey hold operation failed. Retry the hold operation. If it still fails, inspect the service logs.
- `hud-auth-unavailable` — Sign-in unavailable. Configure BETTER_AUTH_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, HUD_ALLOWED_EMAILS, and the AUTH_DB binding before retrying.
- `idempotency-key-holds-another-event` — Idempotency key holds another event. Do not replay this request unchanged: its idempotencyKey already holds an event of another type, and a key names one event. To send this event, send it under a new key; to replay the first one, send it as it was.
- `identity-backfill-unavailable` — Identity backfill unavailable. On partial failure, resume from progress.resumeCursor; otherwise retry the last successful page cursor. Earlier contacts may already have committed; inserts are idempotent. The source binding and complete address index are required; use fill_identity_addresses for an incomplete index.
- `import-list-run-mismatch` — List import page belongs to another run. A runId names one manifest, and its first page binds the run to that page's asOf, alias scope, consent, sourceId and manifest counts. Resend this page with the parameters the run's first page sent; for new parameters, plan a new manifest, whose runId differs. Nothing on this page was written.
- `ingress-failure` — Ingress failure. Retry is safe: delivery is at-least-once and the fold dedupes on idempotencyKey.
- `ingress-transient` — Ingress transient failure. The platform or D1 failed this call for a moment, so the event may or may not be logged and its fold is not confirmed; nothing was refused. Wait the Retry-After seconds and send the same request again. Retry is safe: delivery is at-least-once and the fold dedupes on idempotencyKey.
- `intake-bindings-invalid` — Intake bindings setting invalid. Intake bindings setting invalid: PUBLIC_INTAKE_BINDINGS is set but does not decode, so drovr cannot tell which tenants take double opt-in signups. This event was not applied; earlier events in the same delivery may have been, and redelivery is safe. Fix the setting and the provider's redelivery applies the event.
- `invalid-access-token` — Access token refused. Sign in again: the WWW-Authenticate header names this server's protected-resource metadata. An API key works too.
- `invalid-axiom-alarm` — Unreadable Axiom alarm. Only Axiom's drovr-alarm notifier calls this, with the body template in scripts/axiom-monitors.mjs (ALARM_BODY). A refused body means the notifier drifted: run node scripts/axiom-monitors.mjs --drift.
- `invalid-axiom-alarm-secret` — Invalid Axiom alarm secret. Only Axiom's drovr-alarm notifier calls this: Authorization is Bearer AXIOM_ALARM_WEBHOOK_SECRET, set in the notifier's secret headers.
- `invalid-confirm-token` — Invalid confirmation token. Use the confirmation link from the email as sent. A token cannot be edited or built by hand.
- `invalid-front-signature` — Invalid Front signature. Only Front calls this: a rule webhook's X-Front-Signature is the base64 HMAC-SHA1 of its JSON body, keyed with the Front Webhooks app's API secret.
- `invalid-kit-signature` — Invalid Kit webhook signature. Only Kit calls this: X-Kit-Signature is t=<unix seconds> and one or more v1=<hex HMAC-SHA256 of {t}.{raw body}> keyed with the endpoint secret, within 300 seconds.
- `invalid-unsubscribe-choice` — Invalid unsubscribe request. POST JSON `{ token, choice }` with `choice` `course` or `all`, or the RFC 8058 form body `List-Unsubscribe=One-Click` to `/unsubscribe?t=<token>`.
- `invalid-unsubscribe-token` — Invalid unsubscribe token. Use the unsubscribe link from the email as sent. A token cannot be edited or built by hand.
- `invalid-webhook-signature` — Invalid webhook signature. Only PostShiba calls this: sign {X-Capsule-Timestamp}.{raw body} with the endpoint secret (HMAC-SHA256, hex, sha256= prefix), within 300 seconds.
- `journey-funnel-range-refused` — Journey funnel range refused. Pass from and to as UTC dates (YYYY-MM-DD), to not before from, at most 92 cohort days apart; read a longer span as several ranges. A rebuild takes one real UTC day.
- `journey-funnel-unavailable` — Journey funnel unavailable. The funnel projection's store didn't answer, or a rebuild refused rather than truncate a day (the detail names the operation and why). Reads change nothing: repeat after a short wait. A rebuild that reached a scan limit needs a narrower path, not a retry.
- `journey-reserved` — Journey not writable through /events. Do not post events for this journey: drovr drives it itself. For double opt-in, record a signup with record_signup (POST /signups, tenantId in the body); the confirm link's routes carry the confirmation.
- `kit-subscriber-stops-unavailable` — Kit subscriber stop evidence unavailable. Retry get_kit_subscriber_stops with the same Kit subscriber id. Unknown identity or stop evidence is not an empty stop list; do not admit a signup from this failure.
- `kit-webhook-unavailable` — Kit webhook unavailable. Kit webhooks are not configured on this deployment yet; Kit retries for about 41 hours, so events are not lost.
- `link-target-failure` — Link swap failed. Repeat the same swap; nothing was changed.
- `link-target-not-allowed` — Link target not allowed. Swap to a target on the tenant's allowed link hosts (the same allowlist publish checks); the detail names them. Nothing was changed.
- `main-writes-held` — Writes held for maintenance. drovr is holding every write while its storage moves, so nothing was recorded and nothing was refused. Wait the Retry-After seconds and send the same request again with the same idempotency key. Reads still answer.
- `malformed-contact-sync` — Malformed contact-sync heartbeat. Send {syncedThrough}: an ISO instant, the source's watermark (every contact change up to it pushed and acknowledged).
- `malformed-content-publish` — Malformed content publish. Send {body, layoutId, subject, preview, sender: {name, address}, replyTo?, source} to /tenants/{tenant}/content/messages/{messageId}/versions.
- `malformed-content-read` — Malformed content read. Message and version path segments are exact ids; GET /content/publishes takes message, status (published or refused) and limit.
- `malformed-content-repo` — Malformed content repo request. POST /tenants/{tenant}/content/repo with {access: "read" | "write", ttlSeconds?}, for a tenant with content.
- `malformed-corpus-read` — Malformed corpus read. GET /corpus/pushes/{public|restricted}/{commit}: the corpus repo's visibility and the pushed commit's sha.
- `malformed-corpus-repo` — Malformed corpus repo request. POST /tenants/{tenant}/corpus/repo with {access: read | write, visibility: public | restricted, ttlSeconds?} for a tenant with a content registration.
- `malformed-corpus-search` — Malformed corpus search. GET /corpus/search?query=…&mode=keyword|semantic|hybrid&types=x_post,youtube_video&limit=10: query holds at least one word.
- `malformed-course-restart` — Malformed course restart request. Send one exact {tenantId, contactId, journeyId, evidence, dryRun} with the operator token. evidence is the learner's request by reference (letters, digits, _ . : -; at most 128). dryRun is required.
- `malformed-directory-identities` — Malformed directory identity request. POST JSON with kitSubscriberIds as an array of non-empty strings, capped at 1000 ids.
- `malformed-directory-seed` — Malformed directory seed request. POST JSON with optional non-empty afterId, ISO createdAfter, dryRun, and an integer limit from 1 to 200.
- `malformed-directory-stops` — Malformed directory stops request. Send GET /tenants/{tenant}/directory-stops with an exact tenant id, an optional cursor (nextCursor from the prior page) and a limit from 1 to 500.
- `malformed-event` — Malformed event. POST a JSON EngineEvent shaped like `example`.
- `malformed-event-batch` — Malformed event batch. POST JSON `{events: EngineEvent[]}` holding 1 to `max` items, each shaped like `example.events[0]`.
- `malformed-event-redrive` — Malformed event redrive. Send POST /events/redrive with exactly one of keys (1 to 50 {tenantId, contactId, journeyId, idempotencyKey}), actors (1 to 5 {tenantId, contactId, journeyId}) or force (stage only), an optional dryRun, and allowStale to re-forward events older than 2 days. POST /events/redrive/parked/pass takes seq (a parked seq) and reason (1 to 200 characters, console only).
- `malformed-hold` — Malformed journey hold. POST JSON with one non-empty reason string of at most 500 characters.
- `malformed-inspection` — Malformed inspection request. Use an ISO since/until or relative since such as 2h; windows are capped at 24h. Metric names: wakes, dispatch, folds, storage, queries, delivery.
- `malformed-link-target` — Malformed link swap. POST /tenants/{tenant}/links/targets with {linkId, target, reason} or {messageId, fromTarget, target, reason}.
- `malformed-message-request` — Malformed message request. POST /content/messages/new takes {kind: broadcast, slug?, sentAt?}; POST /content/messages/validate takes {path: messages/<id>.md, text}.
- `malformed-migration` — Malformed migration request. Send {contacts: 1 to 200 exact contact ids} to /tenants/{tenant}/migrations/{planId}/dry-run (with horizonDays?), /apply (with dryRunId), or /rollback.
- `malformed-profile-source` — Malformed profile source. Send {source: live | shadow | profile} for an exact tenant and journey; live is the default and the kill switch.
- `malformed-provider-ramp` — Malformed provider ramp. Send {enabled, percent (0 to 100), cohortStart (ISO instant or null), canaryContacts (at most 50 ids)} for an exact tenant and journey.
- `malformed-provision` — Malformed tenant provision. Send {name, slug, ownerEmail} for an exact tenant id: slug is lowercase letters, digits and hyphens.
- `malformed-requeue` — Malformed provider-sync requeue. Send {rows: [{idempotencyKey, target}]} or {kind, target?, deadAfter?, limit?}: one selection, not both.
- `malformed-retirement` — Malformed retirement request. Send 1 to 200 exact {contactId, journeyId} actor identities from the operator manifest.
- `malformed-send-acceptance` — Malformed send acceptance request. Send one exact {tenantId, contactId, journeyId, idempotencyKey, since, dryRun} with the operator token. dryRun is required.
- `malformed-signup` — Malformed signup. Send {tenantId, contactId, formId, occurredAt, source: {page}, submissionId}; see record_signup's input schema.
- `malformed-stop-backfill` — Malformed value-path stop backfill. Send {tenantId, contactIds: [1 to 50 exact contact ids], dryRun}. Run dryRun:true first.
- `malformed-team-enroll` — Malformed value-path team enrollment. Send {tenantId, contactId, dryRun} and optionally startAt (1 to 7, default 1). Run dryRun:true first.
- `malformed-unfolded-events` — Malformed unfolded-events request. Send GET /tenants/{tenant}/unfolded-events with since and until as ISO instants (since at most 13 days ago, until at least 1 hour ago, since before until), an optional scope (never-born or full), an optional limit (1 to 200 for never-born, 1 to 140 for full) and cursor (nextCursor from the prior page).
- `malformed-wake-poke` — Malformed wake poke. Send either actors (1 to 50 exact {contactId, journeyId} identities on served journeys) or overdue (grace minutes, 0 or more) with an optional integer limit from 1 to 200.
- `mcp-authorization-required` — Authorization required. Send `Authorization: Bearer <api key>`, or sign in with OAuth: the WWW-Authenticate header names this server's protected-resource metadata.
- `membership-unavailable` — Membership could not be checked. Retry the same request in a moment; nothing was refused.
- `migration-blocked` — Migration plan is blocked. A blocked plan moves nobody: fix the plan (stateMap, acknowledgedDrops) until plan_migration shows no blocks.
- `migration-dry-run-mismatch` — No matching migration dry run. Run plan_migration for this tenant, plan and contacts, then apply with its dryRunId within 24 hours.
- `migration-failure` — Migration run failed. Repeat the request; migration runs are recorded, and each actor answers a repeat safely.
- `migration-needs-sign-off` — Migration plan needs a per-call sign-off. A retiming plan needs a per-call sign-off (#9x PR B); until then, keep its timers or split the plan.
- `migration-query-cost-mismatch` — Committed migration cost attribution mismatch. Hold further migrations and inspect storage routing. The run was committed; do not repeat or roll it back.
- `missing-bearer-token` — Missing bearer token. Send `Authorization: Bearer <api key>`.
- `next-cta-off` — Next-best CTA is off for this tenant. The next-cta flag is off for this tenant, so nothing was read. Show the placement's own default. get_feature_flag with key next-cta reads it.
- `next-cta-refused` — Next-best CTA refused. No CTA could be chosen: read the detail. The tenant may have no policy yet (set_cta_policy), the placement may not be in it (get_cta_policy lists them), or the contact has more facts than one request reads. Show the placement's own default.
- `next-cta-unavailable` — Next-best CTA unavailable. The flag, the policy, the contact's facts or the contact's purchases could not be read. Show the placement's own default and ask again later; this call writes nothing.
- `no-matching-links` — No link matches the bulk swap. No static link of that message has that original target yet: check fromTarget against the message's hrefs (get_content_message), or swap one link by linkId. Nothing was changed.
- `operator-auth-unavailable` — Operator authentication unavailable. Configure OPERATOR_TOKEN before running tenant reclamation.
- `operator-unauthorized` — Operator authorization required. Send the configured operator bearer token.
- `pauses-unavailable` — Pauses unavailable. The journey hold store didn't answer; the detail says why. Wait the Retry-After seconds and repeat the read: reads change nothing. A pause's parked count that the HUD hub couldn't read comes back as Unknown inside a 200, not as this problem.
- `product-events-off` — Product events are off for this tenant. Product events are off for this tenant (the product-events flag), so nothing was stored. Keep the events or drop them; do not retry until the flag is on. get_feature_flag with key product-events reads it.
- `product-events-unavailable` — Product events unavailable. The flag or the interest-fact store did not answer, so nothing was stored. Send the same events again: each fact records once by its source id.
- `product-interest-refused` — Product interest dry run refused. The dry run could not score this policy: read the detail. A policy names the caller's tenant, and each excluded product needs a buyers-of deny rule on the policy's campaign (list_recipient_rules). Nothing was written.
- `product-interest-unavailable` — Product interest dry run unavailable. The product facts or buyer rules could not be read. Run the same dry run again; it writes nothing.
- `provision-conflict` — Tenant provision conflicts with the auth store. Provisioning never renames a tenant, moves a slug, or changes a member's role. Check the tenant id and slug, or change the role in the HUD.
- `provision-failure` — Tenant provision failed. Repeat the same provision; it is idempotent.
- `provision-owner-unknown` — Tenant owner has not signed in. Ask the owner to sign in to the HUD once, then repeat the provision.
- `rate-limited` — Rate limit exceeded. Wait the Retry-After seconds, then resume at a steadier rate. Limits are per credential and per client IP; steady traffic above them needs the operator to raise the limit.
- `receipts-unavailable` — Receipt history unavailable. Retry the same read; it changes no state. If the failure persists, ask the operator to inspect receipt-read failure logs. No receipt history was returned.
- `reclamation-failure` — Tenant reclamation failed. Retry the same actor page; reclamation is idempotent and the cursor must not advance before acknowledgement.
- `reclamation-tenant-active` — Tenant is not retired. Add the exact tenant id to RETIRED_TENANTS before reclaiming any actor state.
- `record-stop-address-conflict` — Stop address is ambiguous. No stop was recorded. Use resolve_contacts to inspect the conflicting address claim and ask the identity owner to reconcile it before calling record_stop again. Never guess a contact id.
- `record-stop-unavailable` — Stop recording unavailable. Retry record_stop with the same tenant and address. A stop may already be recorded; retries use its existing suppression episode and safely resume ingress and fan-out. Do not treat unavailable evidence as an unknown address.
- `record-stop-unknown-address` — Unknown stop address. No stop was recorded. Check the tenant and address against the reader's request, then use resolve_contacts to check the corrected address before calling record_stop again.
- `request-traffic-not-configured` — Request traffic evidence not configured. REQUEST_TRAFFIC_TOKEN not bound on this stage
- `request-traffic-refused` — Request traffic read refused. Choose a fixed report, ISO from and to bounds at most 24 hours apart, and catalog route, tool or observed release filters. Narrow a large read; no truncated count is evidence of zero.
- `request-traffic-unavailable` — Request traffic evidence unavailable. Retry the same read. If unconfigured or persistently refused, ask the operator to check the Api-only read credential and account/bucket bindings. Use receipts for committed transition facts; unavailable Basin evidence is not zero traffic.
- `resend-indeterminate` — ResendIndeterminate. Operator reconciliation required: the provider may already have queued this resend. Do not retry with a new resendId or send again.
- `resubscribe-limit` — Re-subscribe limit reached. This address unsubscribed again after it had already re-subscribed once. drovr supports one re-subscribe per contact for now, so no confirmation email was sent and nothing changed. Ask support to subscribe the address again.
- `rollout-step-refused` — Rollout step refused. Provider rollout moves one rung at a time. A profile source goes live, then shadow, then profile once the shadow bar is met (at least 200 matched compares, 0 mismatched, 0 failed counter writes, and matches on 3 distinct UTC days after the day shadow was set). On a route that can still send Kit, a ramp that admits a cohort never takes a cohortStart earlier than the floor the detail names: the cohort already in effect, or the time of the write when none is (a first enable, a re-enable after the kill switch, or a ramp with a null cohort or a percent of 0), because an earlier cohort moves contacts already on Kit to PostShiba mid-sequence. Pass a cohortStart at or after it, or null, or a percent of 0. On a provider-only route, a historical cohortStart is allowed, except one before the kill when re-enabling. A step that loses a race with another write of the same setting is refused; read it again and retry. The kill switches, source live and enabled false, are always accepted.
- `route-chunk-unavailable` — Route code unavailable. This Worker isolate could not load the code for the route, so its handler did not run and nothing changed. Retry the same request; if the same chunk keeps failing, the deployed version is missing it and needs a redeploy.
- `scheduled-health-binding-unavailable` — Scheduled health binding unavailable. Configure the Api SCHEDULED_JOB_WORKFLOW binding, then retry get_scheduled_health. An unbound source is not evidence of missed runs.
- `cut-facts-refused` — Cut fact preview refused. Fix the request as the detail says, then repeat preview_cut_facts. A coverage record must name this tenant, the Kit scope read and every window asked for. Nothing was read.
- `cut-facts-unavailable` — Cut fact preview is unavailable. Retry preview_cut_facts. A failed tenant read classifies nothing, and this read changes nothing.
- `segments-unavailable` — Segment source evidence is unavailable. Retry get_contact_segments or segment_coverage. Failed source reads never become unknown answers, and this read changes nothing.
- `segments-unconfigured` — Segmentation policy is not configured. Configure the tenant's segment dimensions and fact mappings, then repeat get_contact_segments or segment_coverage.
- `send-acceptance-refused` — Send acceptance evidence refused. The latest stored actor.dispatch line for this exact intent must prove an accepted PostShiba email send without a conflict; missing or ambiguous evidence stays pending.
- `send-acceptance-unavailable` — Send acceptance verification unavailable. Retry the same dry run after evidence storage or the actor recovers. No provider send is made by this operation.
- `sequence-needs-registry` — A sequence email needs the registry. Ask for kind: broadcast. A sequence email joins a journey through the registry until the index-flip slice makes journeys/<id>.md the truth.
- `signup-address-held` — Signup address held. drovr could not tie this address to exactly one of your contacts: two contacts share it, or it disagrees with the contact this submission already resolved to. Nothing was recorded or sent, and drovr never merges contacts. Resolve the address with resolve_contacts, then retry the signup with the contactId you mean.
- `signup-counts-range-refused` — Signup counts range refused. Pass from and to as real UTC dates (YYYY-MM-DD), to not before from, at most 31 days apart; read a longer span as several ranges.
- `signup-counts-unavailable` — Signup counts unavailable. The receipt store didn't answer a day's read (the detail says why). The read changes nothing: repeat it after a short wait. Days already cached answer from the cache.
- `signup-identity-unavailable` — Signup identity unavailable. drovr could not resolve this address to a contact right now: the tenant's address index is not complete yet, or contact identity storage did not answer. Nothing was recorded or sent. Retry the same request with the same submissionId; do not mint a contact id instead.
- `signup-lookup-invalid` — Invalid signup lookup. Use a registered tenantId/formId, cursor >= 0 and limit between 1 and 100.
- `signup-lookup-too-large` — Signup cohort exceeds read budget. The full form cohort exceeds the 5000-admission read budget; no partial counts are returned.
- `signup-lookup-unavailable` — Signup evidence unavailable. Verify the intake owner, binding and idx_drovr_provider_sync_signup_lookup migration; deploy the schema only after D3 with an arbiter.
- `signup-lookup-unknown-form` — Intake form not registered. Use a tenant/form pair in PUBLIC_INTAKE_BINDINGS.
- `signup-not-enabled` — Double opt-in signups not enabled for this tenant. This tenant has not turned on double opt-in, so drovr is not taking its signups yet. Nothing was recorded and nothing was sent: keep the signup on its current path until double opt-in is enabled for the tenant.
- `signup-screening-required` — Signup screening required. This form requires your server to screen each submission before it reaches drovr. Verify the visitor with the form's screening provider, then retry with source.screening carrying the provider, its passing verdict, the hostname and when it was verified. Nothing was recorded or sent.
- `signup-verification-refused` — Signup verification refused. This form does not take an address verification, or not by this method. Send the signup without source.verification so it goes through double opt-in, or use a form that declares the method. Nothing was recorded or sent.
- `storage-unavailable` — Application storage unavailable. The request handler did not run. Fix the deployment's storage configuration named in the detail, or retry the same request after the connection recovers.
- `stranded-unavailable` — Owed completions unavailable. The HUD hub's intent ledger didn't answer; the detail says why. Wait the Retry-After seconds and repeat the read: reads change nothing. A hub that hasn't made its first reading yet answers within 5 min.
- `suppressions-request-refused` — Suppressions request refused. Leave every parameter out to read from the beginning, or pass cursor (nextCursor or resumeCursor, unchanged) to continue. limit is a whole number from 1 to 1000. scope is all, course:<journeyId> or course:*. since is an ISO-8601 instant.
- `suppressions-unavailable` — Suppressions unavailable. No page was read; the detail says why. When the store did not answer, wait the Retry-After seconds and repeat the same request: reads change nothing and your cursor stays valid. When the deployment still keeps suppressions on D1, retrying will not help until it reads MySQL.
- `tenant-key-create-unconfirmed` — Key creation not confirmed. Call list_keys before creating another: the key may exist even though this answer could not show it, and its secret cannot be shown again. Revoke any key in the list you did not receive, then create a new one.
- `tenant-key-last-active` — Last active key preflight refused. Create and install a replacement first, or call revoke_tenant_key with confirmLastKey:true to accept that harm. This read-then-act preflight does not serialize concurrent revokes.
- `tenant-key-malformed` — Tenant key input invalid. Use the create_tenant_key, list_tenant_keys or revoke_tenant_key input schema.
- `tenant-key-revoke-unconfirmed` — Key revocation not confirmed. Retry the same revoke: revoking a key that is already revoked changes nothing.
- `tenant-key-unknown` — No such key in this tenant. Call list_tenant_keys and select a key from that tenant.
- `tenant-keys-unavailable` — Tenant key store unavailable. List the keys before retrying. Provision a tenant with an owner before minting its first key.
- `tenant-mismatch` — Tenant mismatch. Post events whose tenantId matches the tenant your key grants.
- `tenant-permission-denied` — Tenant permission denied. This key can read the tenant but not operate it. Read routes still answer; ask the tenant's owner for a key issued with operate.
- `tenant-retired` — Tenant retired. This tenant is retired. Do not retry or create new work for it.
- `timer-reserved` — Timer events not accepted at /events. Do not post timer.fired: drovr fires a journey's timers itself, from the wakes its actor armed. To move a contact on, post the event the journey waits for (a completion, a click or a purchase).
- `unfolded-events-failure` — Unfolded events unavailable. The event log, receipts or actor log could not be read; the detail says why. The read changes nothing: repeat the same page.
- `unknown-api-key` — Unknown or revoked API key. Keys are operator-minted and handed out-of-band. Request one from the drovr operator; a revoked key needs a fresh one.
- `unknown-content-message` — Unknown content message. GET /content/messages lists your tenant's messages.
- `unknown-content-push` — Unknown content push. A push appears once its event is processed; check the commit sha, or push again.
- `unknown-content-version` — Unknown content version. GET /content/messages/{messageId} lists the message's stored versions.
- `unknown-corpus-push` — Unknown corpus push. A push appears once its event is indexed or recorded as failed; check the visibility and the commit sha, or push again.
- `unknown-corpus-source` — Unknown corpus source. Use a source id search_corpus answered; a source outside your access reads as unknown.
- `unknown-form` — Unknown signup form. Use a form registered for your tenant (skills-newsletter for AI Hero). A new form is registered in drovr first.
- `unknown-journey` — Unknown journey. Pick a served journey id from `journeys`.
- `unknown-link` — Unknown link. Give one of the tenant's link ids (from a rendered email's short links), or the bulk form {messageId, fromTarget, target, reason}. Nothing was changed.
- `unknown-migration-plan` — Unknown migration plan. Use a registered plan id; plans are data in git (apps/api/src/migrations.ts).
- `unknown-route` — Unknown route. GET / describes the service.
- `unsubscribe-page-unknown` — No unsubscribe page for this tenant. This tenant has no unsubscribe page. Use the email's one-click unsubscribe (the List-Unsubscribe header), which works for every tenant, or contact support.
- `unsubscribe-unavailable` — Unsubscribe unavailable. Unsubscribe is not configured on this deployment yet. The link stays valid; retry later or contact support.
- `wakes-unavailable` — Wake projection unavailable. Retry. To repair known actors now, poke them by exact identity; that path does not read the projection.
- `webhook-unavailable` — Webhook unavailable. The PostShiba webhook secret is not configured on this deployment yet; PostShiba retries, so events are not lost.
- `world-confirm-link-ambiguous` — No single confirmation to read. Nothing was minted. Read the contact's double-opt-in receipts with get_receipts: none means its signup has not projected yet (read again shortly, or record the signup on the world's fixture form first); two newest at one instant means drive a fresh contact rather than guess which link went out.
- `world-confirm-link-refused` — Not a world confirmation. This read serves only simulated world tenants (world-*) and their fixture confirmation forms, and nothing was read. A real tenant's link reaches its contact by email only; for a world, pass the world tenant and the fixture form its signup used.
- `world-confirm-link-unavailable` — World confirmation link unavailable. The receipt read failed, or the link could not be minted (confirm-doi off for the world, or no signing secret or confirm page on this deployment; the detail says which). Nothing changed: fix the cause and read again.

Served journeys: value-path-skills-course, contact-directory, crash-course-evergreen-offer, shadow-newsletter, double-opt-in, egghead-signup-welcome, egghead-member-welcome
