Update an enrollment (state, lead identity, scheduling)
https://myagentmail.com/enrollments/{id}Two distinct use cases — both supported by this single endpoint:
1. Pause / resume / unenroll
Pass state to move the enrollment between lifecycle states.
active / paused / unenrolled are the values the runner
respects. Terminal states (completed, replied, accepted,
bounced) are set by the runner itself based on signals + the
cadence's exitOnReply setting — don't write to them directly.
2. Fix-up bad enroll-time payload
Use case: a row was enrolled with the wrong shape (missing
leadLinkedinUrl, wrong leadEmail, etc.) and is now stuck on
a step with a NO_TARGET / NO_INBOX error. Patch the
identity fields and the runner picks up the corrected row on
its next tick — no need to unenroll + re-enroll (which would
lose currentStep, lastStepFiredAt, the step_runs audit, and
any leadExternalId joins your side has against the
enrollment).
Merge semantics: only fields present in the request body
are updated. Omitted fields stay as-is. Pass explicit null to
clear a column (where the column is nullable).
Patchable identity fields: leadName, leadEmail,
leadLinkedinUrl, leadProfileUrn, leadExternalId,
inboxId, sessionId. The runner resolves
leadLinkedinUrl → leadProfileUrn automatically when the
next LinkedIn step fires — you do not need to look up the URN
separately before patching.
?retryNow=true query param
When fixing a stuck enrollment, also pass ?retryNow=true to
reset next_step_due_at = NOW() so the runner re-attempts on
the next tick (~1 min) instead of waiting out the post-failure
1h backoff. Opt-in so routine patches (typo fix on a name)
don't unnecessarily trigger an immediate retry.
Successful retry automatically clears lastError from the
list endpoint's per-row summary — the lastError query
ignores failed step_runs that occurred before the most recent
successful run.
Terminal-state guard
Patching lead identity is rejected (409 ENROLLMENT_TERMINAL_STATE)
when the enrollment is in completed / replied / accepted /
bounced / unenrolled. There's no use case for "fix a lead
on a completed cadence" — the runner won't re-run it. If you
genuinely want to resume, PATCH state: 'active' first, then
patch identity in a second call.
leadExternalId conflict
Changing leadExternalId to a value another enrollment in the
same cadence already uses returns 409 LEAD_EXTERNAL_ID_CONFLICT
with the conflicting value in the response body. The
(cadence_id, lead_external_id) unique constraint is what
makes the enroll endpoint idempotent — you can't violate it
via PATCH either.
Worked example: fix-up flow
Before — list endpoint shows:
{
"id": "01HZ…",
"leadName": null,
"leadLinkedinUrl": null,
"state": "active",
"currentStep": 0,
"stepsCompleted": 0,
"lastError": {
"code": "NO_TARGET",
"message": "no_linkedin_url",
"atStepIndex": 0,
"firedAt": "2026-05-19T14:00:00Z"
},
"nextStepDueAt": "2026-05-19T15:00:00Z"
}
Patch request:
PATCH /v1/enrollments/01HZ…?retryNow=true
Content-Type: application/json
{
"leadName": "Jane Doe",
"leadLinkedinUrl": "https://www.linkedin.com/in/jane-doe-1a2b3c/",
"leadEmail": "jane@acme.com"
}
Response (immediate — lastError clears on the next list call
after the runner ticks successfully):
{
"ok": true,
"enrollment": {
"id": "01HZ…",
"leadName": "Jane Doe",
"leadLinkedinUrl": "https://www.linkedin.com/in/jane-doe-1a2b3c/",
"leadEmail": "jane@acme.com",
"state": "active",
"currentStep": 0,
"nextStepDueAt": "2026-05-19T14:32:11Z",
"updatedAt": "2026-05-19T14:32:11Z"
}
}
Path parameters
idrequired string |
Query parameters
retryNowboolean | When true, also resets `next_step_due_at = NOW()` so the runner picks the enrollment up on the next tick instead of waiting out the post-failure 1h backoff. |
Request body
Content-Type: application/json
state"active" | "paused" | "unenrolled" | Move between lifecycle states. Terminal states are set by the runner; don't write to them. enum: "active" | "paused" | "unenrolled" |
exitReasonstring | |
leadNamestring | Merge semantics: omit to keep, null to clear, string to set. |
leadEmailstring | format: email |
leadLinkedinUrlstring | format: uri |
leadProfileUrnstring | `urn:li:fsd_profile:…`. Optional — the runner resolves this from `leadLinkedinUrl` automatically when the next LinkedIn step fires. |
leadExternalIdstring | Idempotency key. Changing it to a value another enrollment in this cadence already uses returns 409 LEAD_EXTERNAL_ID_CONFLICT. |
inboxIdstring | format: uuid |
sessionIdstring | format: uuid |
currentStepinteger | Manual cursor advance/rewind. Use sparingly — the runner manages this automatically. Legitimate cases: 1. Bypass an `after_accept`-gated step when the lead accepted out-of-band (rare). 2. Replay from a step that fired against the wrong inbox / session, after fixing the enrollment. 3. Abandon a step stuck on an external signal that won't materialise. Range: `[0, stepCount]` inclusive. Passing `stepCount` transitions the enrollment to `state: completed` (`exit_reason: manually_completed`, `next_step_due_at` cleared). Out-of-range values return `400 STEP_OUT_OF_RANGE` with the cadence's actual step count. Audit: every manual advance inserts a `linkedin_cadence_step_runs` row with `status: skipped`, `skip_reason: manual_advance_via_patch`, anchored to the step the cursor was on. Visible in `GET /v1/enrollments/{id}`'s `stepRuns[]` so anyone auditing the cadence sees the jump. Risk: rewinding to an earlier email step may cause a duplicate send when the runner re-fires. LinkedIn dedups invitations server-side; SMTP does not. Pair with `state: paused` first if you want to inspect before letting the runner re-fire. |
Responses
errorstring | |
code"ENROLLMENT_TERMINAL_STATE" | "LEAD_EXTERNAL_ID_CONFLICT" | enum: "ENROLLMENT_TERMINAL_STATE" | "LEAD_EXTERNAL_ID_CONFLICT" |
statestring | On ENROLLMENT_TERMINAL_STATE: the current state blocking the patch. |
conflictingExternalIdstring | On LEAD_EXTERNAL_ID_CONFLICT: the leadExternalId that's already in use. |
hintstring | Human-readable next-step suggestion. |
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.