All endpoints

Get an enrollment + its step-run history

GEThttps://myagentmail.com/enrollments/{id}

Returns the enrollment row plus its stepRuns[] history (last 50, newest-first). Use this when you need per-step status — completed, skipped, failed — to render a step-by-step ✅ timeline.

Step run statuses

  • sent — step fired and LinkedIn / SMTP accepted the action. Render as a green checkmark.
  • skipped — step was evaluated but its condition didn't apply (e.g. after_accept step on an unaccepted invitation, or never_replied step when the lead replied). Normal cadence flow — render as a grey checkmark or skip-icon, NOT as an error.
  • failed — runner tried and got back ok:false from the downstream call. errorCode is set. The enrollment is deferred 1h and retried on the next tick — this is NOT a terminal state on its own.
  • queued — internal scheduling state. You'll rarely see this in practice; included for completeness.

Error codes you'll see in errorCode

Set by api/src/lib/cadences/runner.ts. The catalogue:

  • NO_SESSION_AVAILABLE — workspace has zero connected LinkedIn sessions. Customer needs to connect one at /dashboard/linkedin.
  • WORKSPACE_QUOTA_EXHAUSTED — sessions exist but all at the per-action daily cap (e.g. 30 connection_requests/day per session). Resets at midnight UTC, or add more sessions.
  • PINNED_SESSION_UNUSABLEenrollment.sessionId was set manually but that session is revoked / not-active / capped. Customer should reconnect or repin.
  • NO_TARGETleadLinkedinUrl (for LinkedIn steps) or leadEmail (for email steps) was missing at enrollment.
  • NO_INBOX — email step but no inboxId on the enrollment or cadence.
  • DRAFT_WEBHOOK_FAILED — step has draftStrategy: "webhook" and the customer's draft handler timed out (>30s), 5xx'd, returned non-JSON, returned an invalid defer, or returned no body. errorMessage carries the upstream detail (which status code, "no body in response", etc). The step defers 1h and retries — which means a flaky handler will keep getting opportunities to succeed. To stop retries, mark the enrollment state: "paused" or unenroll.
  • AI_STRATEGY_REMOVED — pre-existing step still on draftStrategy: "ai" (removed 2026-05-19). Update the step to static (with staticBody) or webhook (with the cadence's webhookUrl set to your draft handler).
  • send_failed — downstream LinkedIn / SMTP returned an error. errorMessage carries the upstream detail.

Worked example response

{
  "ok": true,
  "enrollment": {
    "id": "01HZ…",
    "cadenceId": "01HY…",
    "leadExternalId": "delegated:lead:abc-123",
    "leadName": "Jane Doe",
    "leadLinkedinUrl": "https://www.linkedin.com/in/jane-doe-1a2b3c/",
    "leadProfileUrn": "urn:li:fsd_profile:ACoAAA…aaaa",
    "sessionId": "01HX…",
    "currentStep": 3,
    "state": "active",
    "nextStepDueAt": "2026-05-21T14:00:00Z",
    "lastStepFiredAt": "2026-05-19T14:00:00Z",
    "enrolledAt": "2026-05-18T10:30:00Z",
    "updatedAt": "2026-05-19T14:00:00Z"
  },
  "stepRuns": [
    {
      "id": "01HZ…",
      "stepIndex": 2,
      "stepKind": "linkedin_message",
      "status": "sent",
      "channelUrn": "urn:li:msg_message:(…)",
      "renderedSubject": null,
      "renderedText": "Hey Jane — saw your post about cold outbound…",
      "errorMessage": null,
      "errorCode": null,
      "skipReason": null,
      "firedAt": "2026-05-19T14:00:00Z"
    },
    {
      "id": "01HZ…",
      "stepIndex": 1,
      "stepKind": "wait",
      "status": "sent",
      "channelUrn": null,
      "renderedSubject": null,
      "renderedText": null,
      "errorMessage": null,
      "errorCode": null,
      "skipReason": null,
      "firedAt": "2026-05-19T13:00:00Z"
    },
    {
      "id": "01HZ…",
      "stepIndex": 0,
      "stepKind": "linkedin_connect",
      "status": "sent",
      "channelUrn": "urn:li:fsd_profile:ACoAAA…aaaa",
      "renderedSubject": null,
      "renderedText": "Hi Jane — common interest in outbound; thought I'd connect.",
      "errorMessage": null,
      "errorCode": null,
      "skipReason": null,
      "firedAt": "2026-05-18T10:30:00Z"
    }
  ]
}

Rendering ✅ progress

const r = await mam.getEnrollment({ id });
const sentByIndex = new Map<number, true>();
const skippedByIndex = new Map<number, true>();
let lastFailure: typeof r.stepRuns[number] | null = null;

for (const run of r.stepRuns) {
  if (run.status === "sent") sentByIndex.set(run.stepIndex, true);
  else if (run.status === "skipped") skippedByIndex.set(run.stepIndex, true);
  else if (run.status === "failed" && !lastFailure) lastFailure = run;
}

// For each step in cadence.steps[]:
//   if (sentByIndex.has(i))    → ✅ done
//   else if (skippedByIndex.has(i)) → ⊘ skipped (condition not met)
//   else if (i === enrollment.currentStep && lastFailure) → ⚠ blocked
//   else if (i === enrollment.currentStep) → ⏳ pending
//   else → ◯ upcoming

Path parameters

id
required
string

Responses

200application/jsonEnrollment + step runs.
ok
boolean
enrollment
object

Same shape as items in `/cadences/{id}/enrollments` (minus the `stepsCompleted` / `lastError` summary — drill into `stepRuns[]` below instead).

runnerState
object

Read-only diagnostic — answers "would the runner fire this row right now? if not, why?". Same logic the runner uses to pick enrollments (`pickDueEnrollments` + `evaluateCondition`), replicated read-only so callers can self-diagnose without poking the runner. Use this BEFORE filing a support ticket. If `pickable: true` and the row isn't firing, the runner tick interval is 5 minutes — `maxExpectedWaitSeconds` carries the upper bound. Use `POST /v1/cadences/{id}/run-now` to force a tick. If `pickable: false`, `reasons[]` lists every blocking condition with the exact PATCH curl needed to unblock. `details` exposes the underlying values (state, scheduling, condition evaluation) for deeper inspection.

pickable
boolean

True iff the runner would pick this enrollment on its next tick.

reasons
string[]

Empty when pickable=true. Each item is a human-readable explanation of a blocking condition, including the corrective curl when applicable.

details
object

Underlying decision inputs — state, scheduling, condition evaluation, draft strategy sanity.

state
string

enrollment.state

cadenceEnabled
boolean
nextStepDueAt
string

format: date-time

nextStepDueInSeconds
integer

Negative = overdue, positive = future.

currentStep
integer
stepCount
integer
currentStepKind
string
currentStepCondition
string
currentStepDraftStrategy
string
conditionOutcome
"proceed" | "skip-step" | "wait"

enum: "proceed" | "skip-step" | "wait"

conditionReason
string
runnerTickIntervalSeconds
integer

How often the runner ticks (currently 300s/5min).

maxExpectedWaitSeconds
integer

Upper bound on time-to-fire when pickable=true. Null when blocked (no deterministic ETA).

stepRuns
object[]

Last 50 step runs for this enrollment, newest-first.

id
string

format: uuid

stepIndex
integer

0-based; matches cadence.steps[stepIndex].

stepKind
"linkedin_connect" | "linkedin_message" | "email" | "wait"

enum: "linkedin_connect" | "linkedin_message" | "email" | "wait"

status
"sent" | "skipped" | "failed" | "queued"

enum: "sent" | "skipped" | "failed" | "queued"

channelUrn
string

For LinkedIn steps: invitation URN / message URN / profile URN. For email: the message-id.

renderedSubject
string

Email subject (email steps only).

renderedText
string

The body that was actually sent — AI-generated or static, post-rendering.

errorMessage
string
errorCode
string

See description above for the catalogue.

skipReason
string

Why a `skipped`-status run was skipped (e.g. condition_not_met).

firedAt
string

format: date-time

404Not found.

Authentication

Send your API key in the X-API-Key header (or Authorization: Bearer <key>). Any prefix accepted by this endpoint — tk_, wk_, ak_, or sa_ — is documented in the key prefix table.