Skip to main content

Bounded outreach

Bounded outreach turns an operator’s explicit selection from a reviewed lead snapshot into an auditable SMS batch. Erdo checks every selected person, shows every exclusion, fixes the message and complete selection into a preview, and sends only after a separate arm request confirms the exact preview a person reviewed. Erdo never chooses the first N people or splits a selection into automatic batches. This is a deliberately narrow surface for controlled outreach. It is not a campaign builder: there are no editable cadences, branches, recurring schedules, personalization rules, or automatic follow-ups.
Every REST request and MCP connection for outreach requires a validated X-Project-ID. Omitting it is an error; Erdo never falls back to all projects. The authenticated user must also be a persisted owner or admin of the active organization; ordinary members and project-scoped external tokens cannot read consent evidence or control provider actions. The bounded_outreach rollout must also be enabled for the caller. Outside the rollout, reads and writes return a clear setup error rather than an empty list.

How one batch works

  1. Record explicit consent evidence for the recipients you may contact.
  2. PUT the exact 1 to 700 recipients selected by the operator into a named draft. The 700-person limit is a pilot safety ceiling, not a default or automatic target.
  3. Read the returned recipients, exclusions, final message, provider blockers, preview_revision, eligible_count, and message_hash.
  4. Arm that exact preview with a replay-safe action reference.
  5. Read recipient receipts as Erdo records provider acceptance, delivery, failure, replies, and opt-outs.
Creating or reading a draft never sends a message. Erdo appends Reply STOP to opt out. to the reviewed text and returns the final body and hash. Eligible recipients are assigned deterministically: one-fifth, rounded up, is held for seven days and the rest is due immediately. For any authorised eligible set, the delayed count is ceil(eligible_count / 5) and every other eligible recipient is immediate. Consent is a separate resource because a boolean in a batch would only repeat the caller’s claim. A grant records where the evidence came from, which version of that evidence was reviewed, the stable lead identity and phone it covers, and when consent was granted.
The pilot accepts explicit_opt_in only. evidence_text is required and stores the exact consent wording or source excerpt that was reviewed, up to 4,000 characters. Replaying the same grant_ref and evidence returns the stored grant. Reusing it for different evidence returns a conflict. Revoke without deleting the audit record:
List grants with GET /v1/outreach-consents?limit=25; pass the returned next_cursor as cursor for the next page. Read one with ?grant_ref=crm-consent-lead-104-v1, or with recipient_ref, phone_raw, and default_country when the source does not already know the grant reference.

Create the immutable preview

The batch slug is the resource address inside the active project. The source snapshot, initiative and sequence step are part of the immutable content:
Erdo, not the caller, normalizes phone_raw, matches the consent ledger, checks generic SMS and outreach-wide suppression, checks prior contact for the same initiative step, and assigns cohorts. Missing evidence does not reject the preview. The recipient remains visible with eligibility_state: "excluded" and an exclusion_reason such as:
  • ambiguous_identity
  • invalid_phone
  • duplicate_recipient_ref
  • duplicate_canonical_phone
  • cross_batch_duplicate_recipient_ref
  • cross_batch_duplicate_canonical_phone
  • channel_suppressed or outreach_suppressed
  • prior_sequence_step_contact
  • prior_sequence_step_selection
  • no_matching_explicit_consent
Provider and contact-hours setup are also preview facts. provider_readiness checks the dedicated outreach Twilio account and number, its pin to the active project, registration approval, the HTTPS status callback, and the independent dispatch switch. policy_readiness checks that the organization has enabled a valid contact-hours window and timezone. A failed check remains visible in its blockers; armable stays false and arm_blockers contains the combined reasons. The provider blocker codes are twilio_sms_outreach_account_sid_missing, twilio_sms_outreach_account_not_dedicated, twilio_root_credentials_missing, twilio_sms_outreach_phone_number_missing, twilio_sms_outreach_project_mismatch, twilio_status_callback_not_configured, twilio_sms_outreach_registration_not_approved, and twilio_sms_outreach_dispatch_not_enabled. Contact-hours policy reports outreach_contact_hours_policy_missing, outreach_contact_hours_policy_unavailable, or outreach_contact_hours_policy_disabled, or the more specific outreach_contact_hours_window_invalid and outreach_contact_hours_timezone_invalid. provider_cost_estimate shows the configured USD price per segment, segments per recipient, estimated segment count, and estimated cost for the eligible audience. When pricing is not configured, available is false and unavailable_reason explains the missing fact; Erdo does not invent a cost. An operator may create another explicit batch for the same initiative_ref and sequence_step after an earlier batch is armed. Erdo never broadens that later selection: anyone already contacted is excluded as prior_sequence_step_contact, while anyone still reserved by an active earlier batch is excluded as prior_sequence_step_selection. Duplicate draft selections remain visibly excluded rather than silently choosing one copy. The lifetime initiative-step pool cannot grow beyond 700 distinct recipient references, so multiple batches cannot bypass the pilot safety ceiling. Repeating the PUT with the same immutable input re-evaluates consent, suppression, prior contact, cross-batch duplicates, cohorts, counts, and the preview revision across the still-draft initiative step. It never sends. Changing the source, recipients, message, version, initiative, or step under the same slug returns a conflict; create a new batch slug for a new review.

Arm, pause, resume, or cancel

Actions use one endpoint and always require a stable action_ref. Repeating an action reference returns its stored receipt; it never performs the transition again. Arming also compares the facts shown during review:
If any expected value differs, nothing starts. pause, resume, and cancel bind the click to the exact immutable source, state, and batch version shown to the operator:
Use all three expected values from the same batch read. If the request outcome is unclear, retry the identical body. Erdo returns the original receipt with a stable acted_at; changing the body under the same action_ref is rejected. A new action reference with stale facts is also rejected, even if a later pause/resume cycle returns the batch to the same named state. These actions affect contact that has not reached the provider and cannot recall a message already accepted by the provider.

Read batches and recipient receipts

Each list item includes batch, counts, armable, arm_blockers, provider_readiness, policy_readiness, and provider_cost_estimate. A workspace can therefore render factual totals and blockers without fetching every batch detail first. Batch state is draft, running, holding, paused, cancelled, or completed. Recipient delivery state distinguishes draft, excluded, queued, sending, provider_accepted, delivered, failed, held, outcome_unknown, and cancelled. Response state is separate: none, replied, or opted_out. Do not treat delivery as a reply or add overlapping counts together. The batch summary’s contacted value is the unique number of recipients accepted by the provider or beyond. outcome_unknown is a separate delivery state for the dangerous case where a provider request may have succeeded but Erdo did not receive a conclusive response. Erdo pauses the whole batch and will not send or resume it while that receipt is unresolved. Every send includes signed per-recipient scope in its status callback URL, so a later provider callback can attach the missing message ID and repair the message ledger without sending again. resume remains blocked until that callback resolves the unknown state; a retry never guesses that the original send failed. Use initiative_ref when a workspace belongs to one outreach initiative. Erdo applies the filter before pagination, so an app never fetches the newest page across unrelated initiatives and then drops the batch it needed. Recipient states are deliberately not one ambiguous server filter: eligibility, delivery, and response are separate axes. Page through the complete immutable selection with limit=100 and each returned next_cursor, then filter the exact axis in the client (eligibility_state, delivery_state, or response_state). Summary counts and arm validation always cover the complete selection, never only the current page. A STOP reply marks the receipt opted out, suppresses the stable lead across later batches in the project, and cancels any queued or held contact for that lead. Replies that cannot be tied to a stored outbound message are not attached to a guessed batch.

MCP tools

The MCP surface uses the same authenticated project and the same service logic:
  • erdo_put_outreach_batch
  • erdo_list_outreach_batches
  • erdo_get_outreach_batch
  • erdo_list_outreach_batch_recipients
  • erdo_act_on_outreach_batch
  • erdo_put_outreach_consent
  • erdo_list_outreach_consents
  • erdo_get_outreach_consent
Add X-Project-ID to the MCP connection headers. A raw MCP client cannot use a scoped API key; that existing fail-closed rule remains unchanged.

CLI

Every outreach command requires the global --project context. Use --org as well when the key can operate more than one organization.
For revocation, the consent file contains only grant_ref and "revoked": true. List commands accept --limit and --cursor.

Scoped API keys

A server can mint a key with outreach:read, outreach:send, or both. outreach:send covers consent writes, draft creation, and lifecycle actions. It is deliberately not implied by resources:write: arming outreach can contact people and spend money. Both capabilities remain confined to the authenticated project header and still require the key’s user to be a persisted organization owner or admin.