Skip to content

Sequences & enrollments

In Model A you decide when a call happens. In Model B you tell Pace what happened

  • a claim was handed over, a payment arrived - and a versioned escalation ladder decides what to do about it.

Three objects:

Signal

A business fact you emit. payment.received, claim.handed_over.

Sequence

A versioned template: the ladder of wait, call and exit steps.

Enrollment

One subject on one ladder. The running instance.

A signal is a fact, not an instruction. You are not saying “call this person”; you are saying “this is what happened”, and the ladder decides.

Terminal window
curl -sS -X POST https://api.pacepayments.ai/v1/signals \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"signal_type": "payment.received",
"subject_reference": "CUST-88213",
"dedupe_key": "payment.received:INV-2026-118442",
"occurred_at": "2026-08-30T08:12:00+02:00"
}'

signal_type is your own taxonomy in dot.case. Three types the platform interprets itself, each of which stops a running ladder immediately:

Type Meaning
payment.received The debt was paid.
claim.withdrawn The creditor pulled the file.
claim.disputed A written objection arrived.

Everything else is yours to define and only means what your sequences say it means.

dedupe_key is a business key, not a technical one. A repeat returns the original signal with duplicate: true and triggers nothing again.

It is scoped to (tenant, signal_type) and retained for 90 days - considerably longer than the 48-hour Idempotency-Key window. That difference is the whole point: Idempotency-Key protects you from a retried HTTP request, while dedupe_key protects you from an at-least-once producer that re-emits the same fact days later because a queue was replayed. See Idempotency.

occurred_at orders this signal against others. Send it when the fact happened earlier than the API call - a nightly batch reporting yesterday’s payments should carry yesterday’s timestamps, or the ordering against a call that happened in between comes out wrong.

A sequence is a template with versions. Steps run in order.

{
"key": "standard_escalation",
"title": "Standard escalation ladder",
"trigger": {
"signal_type": "claim.handed_over",
"enrollment_policy": "one_active_per_subject"
},
"exit_conditions": [
{ "type": "signal", "signal_type": "payment.received" },
{ "type": "suppression" }
],
"steps": [
{ "id": "settle", "type": "wait", "after": { "min": "P2D" } },
{ "id": "first", "type": "call", "order_type": "zahlungserinnerung" },
{ "id": "cool", "type": "wait", "after": { "min": "P5D", "max": "P7D" } },
{ "id": "second", "type": "call", "order_type": "mahnung" },
{ "id": "give_up", "type": "exit", "reason": "ladder_exhausted" }
]
}

wait holds - and never longer than the calling window permits. The computed instant is intersected with the statutory window at execution time, so the actual wake-up can fall later than after.max. That is not imprecision; it is the rule that client configuration narrows and never widens.

Durations are ISO 8601, days/hours/minutes only. No months or years: those have no fixed length, and a wait window that comes out differently depending on the month is not a wait window.

call creates an order and waits for its result. The step does not dial by itself. It creates an ordinary call order that runs the full Model A lifecycle and emits the same events as one you submitted directly - which is why Model B has no second dial path and no second result derivation.

exit ends the enrollment. reason is recorded verbatim as the enrollment’s exit_reason.

one_active_per_subject (the default) or replace_active.

There is deliberately no allow_parallel. Running two ladders against the same person at once only raises contact pressure, and that is the one thing this field must not be able to do.

exit_conditions stop a ladder wherever it has got to. A suppression condition is always implicitly present - a do-not-call entry halts every sequence whether or not you list it. You may write it out for clarity.

stateDiagram-v2
    direction LR
    [*] --> draft
    draft --> active: activate
    active --> archived: archive

A draft can also be archived directly, without ever being activated.

Only a draft can be activated, an active version is immutable, and at most one version is active at a time.

An enrollment pins its version when it is created and never changes it. Activating a new version leaves running enrollments alone. This is what lets you edit a ladder without altering the behaviour of people already partway down the old one - otherwise a change on Tuesday would silently rewrite what someone had been promised on Monday.

Archiving means no new enrollments. Running ones finish.

An enrollment is one subject on one ladder.

stateDiagram-v2
    direction LR
    [*] --> active
    active --> paused: pause
    paused --> active: resume
    active --> completed
    completed --> [*]

That is the ladder running to its end. Three other endings are reachable from active and, except for the first, from paused as well:

To When
exited An exit condition or skip_remaining_if matched.
canceled You cancelled the enrollment.
failed The sequence engine could not advance it.

completed and exited are both ordinary endings and mean different things: completed is a ladder that ran to its end, exited is one stopped early by a condition. Read exit_reason for which - it is machine-readable (goal_met:payment.received, suppressed, client_cancel, superseded) and kept separate from status so a typed client can branch on one and log the other.

call_order_ids lists the orders the ladder created, oldest first. You can also reach them with GET /v1/call-orders?enrollment_id=….

Operation Effect
POST /v1/enrollments/{id}/pause Suspends an active enrollment. The runner stops touching it.
POST /v1/enrollments/{id}/resume Returns a paused enrollment to active.
POST /v1/enrollments/{id}/cancel Ends it. Pending call orders are cancelled too.

Cancelling from a terminal state returns 409 enrollment_not_controllable.

You can do all of this today, and it is worth doing now:

  1. Emit your signals for real

    Wire the producer, send real dedupe_key values, and confirm that duplicates come back with duplicate: true. Nothing calls anyone.

  2. Model your ladders and activate a version

    Creating and activating sequences works. Getting the shape right - and seeing what your own escalation policy looks like written down - is the slow part.

  3. Subscribe to enrollment events

    enrollment.* events exist in the registry. Subscribe now so that when execution is switched on there is no second integration step.