Docs · v1

Enroll API

Product-triggered outbound. CSV you already have, or POST /api/enroll when the product fires. Sequences draft with your LLM key and leave from SMTP you own. v1 does not require a Bearer API key — use a session cookie or the optional workspace header.

No scraperNo LinkedIn automationNo owned prospect databaseNo inbox warmup as the productNo guaranteed inbox

POST /api/enroll

JSON body. email and provenance.source are required. sequenceId is an optional UUID — omit it to use the workspace default sequence. Tags merge onto the contact.

curl -s -X POST $APP_URL/api/enroll \
  -H 'content-type: application/json' \
  -H 'x-enrollmill-workspace: you@company.com' \
  -d '{
    "email": "casey@example.com",
    "sequenceId": "11111111-1111-1111-1111-111111111111",
    "tags": ["trial"],
    "provenance": {
      "source": "usagemill.overage",
      "eventId": "evt_01HQ",
      "mill": "usagemill"
    }
  }'

200 returns { ok, enrollmentId, contactId, sequenceId, status, idempotent }. Invalid JSON or payload is 400. If CostSlice reports that tenant unprofitable, the response is 409 with Paused — tenant looks unprofitable in CostSlice. A thin tenant stays paused until hitl.approve is approve. Forcing an unprofitable tenant requires hitl.approve of force and an actor. Same-event retries that already enrolled skip this check.

Workspace (no Bearer key in v0)

Auth is soft. There is no API key header yet. Resolution order:

  1. Signed-in session cookie, if present.
  2. Optional header x-enrollmill-workspace (email). Looks up an existing workspace — it does not create a user.
  3. Otherwise the first existing user in the database. No fallback mint.

Idempotency

When provenance.eventId is set, retries with the same workspace + event return the existing enrollment and idempotent: true. Without an event id, the key is workspace + email + sequence + source.

Stop on reply, bounce, or unsubscribe

These are not log-only stubs. A matching contact's latest enrollment becomes replied, bounced, or unsubscribed. Queued send jobs for that enrollment are marked stopped. The send worker refuses further sends. Reply timeline entries store the reply text.

curl -s -X POST $APP_URL/api/webhooks/bounce \
  -H 'content-type: application/json' \
  -d '{"email":"casey@example.com","reason":"mailbox full"}'

curl -s -X POST $APP_URL/api/webhooks/reply \
  -H 'content-type: application/json' \
  -d '{"email":"casey@example.com","text":"not now — maybe later"}'

curl -s -X POST $APP_URL/api/webhooks/unsubscribe \
  -H 'content-type: application/json' \
  -d '{"email":"casey@example.com","reason":"please stop"}'

Bounce and unsubscribe return { ok: true, recorded: true, stopped, contactId, enrollmentId }. Reply is classified as inbound: the sequence stops, then a summary plus two draft variants land on /approvals. Nothing sends until a human approves.

POST /api/webhooks/inbound

Resend inbound (Media Yard dogfood). Accepts Resend email.received JSON or a simple { from, to, subject, text, messageId } body. Idempotent on workspace + Message-ID. Classify:

  • reply — stop the sequence, then Approvals pending. BYO LLM writes the summary plus Short / Helpful drafts when a key is present; otherwise a stub summary and a manual composer. Never auto-send.
  • bounce / unsub — stop only. No AI drafts.
  • other — timeline only. v1 drafts are reply-only.
curl -s -X POST $APP_URL/api/webhooks/inbound \
  -H 'content-type: application/json' \
  -d '{
    "from": "casey@example.com",
    "to": "you@yourproduct.com",
    "subject": "Re: your trial is live",
    "text": "not now — maybe later",
    "messageId": "<unique@example.com>"
  }'

If RESEND_WEBHOOK_SECRET / inbound env is missing, the route still accepts JSON and signed-in /approvals can simulate a reply. IMAP poll is out of v1. No Slack notify, no hosted LLM, no LinkedIn or SMS.

GET /api/health

curl -s $APP_URL/api/health
# {"ok":true}

CSV or enroll API

Both paths are first-class. Import a file you already have, or enroll on provenance.eventId.

Mill recipes

Media Yard mills enroll through the same API external customers get.

CostPerJob

Job closed → collection

A closed job on CostPerJob enrolls the customer into a receipt and review sequence on your domain.

curl -s -X POST $APP_URL/api/enroll \
  -H 'content-type: application/json' \
  -d '{"email":"casey@example.com","provenance":{"source":"costperjob.job_closed","eventId":"evt_01HQ","mill":"costperjob"}}'
UsageMill

Overage → upgrade

Usage cliff or overage event posts /api/enroll. Sequence argues the upgrade, not a rented list.

curl -s -X POST $APP_URL/api/enroll \
  -H 'content-type: application/json' \
  -d '{"email":"casey@example.com","provenance":{"source":"usagemill.overage","eventId":"evt_01HQ","mill":"usagemill"}}'
Webhookmill

Webhook → enroll

A signed webhook from your stack becomes provenance. Retries stay idempotent on eventId.

curl -s -X POST $APP_URL/api/enroll \
  -H 'content-type: application/json' \
  -d '{"email":"casey@example.com","provenance":{"source":"webhookmill.signed","eventId":"evt_01HQ","mill":"webhookmill"}}'
Mentionmill

Mention → hello

A known account mentioned you. Enroll the address you already have — no scrape, no purchased dump.

curl -s -X POST $APP_URL/api/enroll \
  -H 'content-type: application/json' \
  -d '{"email":"casey@example.com","provenance":{"source":"mentionmill.known_mention","eventId":"evt_01HQ","mill":"mentionmill"}}'
CostSlice

Spend spike → P&L note

CostSlice flags a slice. Finance gets a sequence with the event attached, sent on their reputation.

curl -s -X POST $APP_URL/api/enroll \
  -H 'content-type: application/json' \
  -d '{"email":"casey@example.com","provenance":{"source":"costslice.spend_spike","eventId":"evt_01HQ","mill":"costslice"}}'
Askforstars

Happy path → stars

A successful outcome enrolls a short ask-for-stars sequence. Stop on reply. No review spam farm.

curl -s -X POST $APP_URL/api/enroll \
  -H 'content-type: application/json' \
  -d '{"email":"casey@example.com","provenance":{"source":"askforstars.happy_path","eventId":"evt_01HQ","mill":"askforstars"}}'

Secrets and AUTH_SECRET

LLM keys and SMTP passwords are stored encrypted with AUTH_SECRET. Add them in Account → Secrets after sign-in. The public homepage does not walk the wizard.

Domain and inbox checklist

Mail should leave on a domain you control. Tick these yourself — Enrollmill does not auto-verify DNS and does not warm inboxes.

SPF

A TXT record that says which mail servers may send as you.

v=spf1 include:your-smtp-host -all

DKIM

A cryptographic signature so receivers can tell the message really came from your domain.

Add the CNAME or TXT selector your inbox host issued (often s1._domainkey).

DMARC

A policy that tells receivers what to do if SPF or DKIM fail. Start gentle, then tighten.

v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com

Anti-claims

We will not sell a scrape, a LinkedIn farm, an owned prospect database, inbox warmup as the hero feature, or a guaranteed inbox. Enrollmill enrolls people your product already knows, on the customer's reputation.

Media Yard LLC · PO Box 73, Pennsauken, NJ 08110 · Terms · Privacy · Support