Signal
A business fact you emit. payment.received, claim.handed_over.
In Model A you decide when a call happens. In Model B you tell Pace what happened
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.
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 the important fielddedupe_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.
enrollment_policyone_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:
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.
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.
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.