Update an enrollment (state, lead identity, scheduling)
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"
}
}
Request
- Base URL: https://anjal.app
- URL: https://anjal.app/enrollments/{id}
- Auth: API key in header X-API-Key
Path parameters
Query parameters
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
Response
Updated. Returns the full enrollment in the same shape as GET /enrollments/{id}'s enrollment field.
Changes
No recorded changes to this operation across all 1 revision of this API.