Testing in the sandbox
The sandbox host runs simulated telephony. No telephone rings, and no real person is contacted, whatever number you put on the order.
https://sandbox.api.pacepayments.aiSandbox credentials are separate from live ones. See Environments.
The nine magic numbers
Section titled “The nine magic numbers”Dial one of these and you get a predictable outcome. Each row is pinned by a test in Pace’s own suite, and the test asserts the derived result, not the script.
| Number | Result |
|---|---|
+4989000000001 |
Conversation → payment_promised, reached / conversation |
+4989000000002 |
Answered, hung up immediately → hangup_no_interaction |
+4989000000003 |
Identified, then hung up → identified_then_hangup |
+4989000000004 |
no_answer on every attempt → not_reached after the retry policy |
+4989000000005 |
Voicemail on every attempt → voicemail_only |
+4989000000006 |
Third party answers → wrong_party / not_debtor |
+4989000000007 |
Objection already_paid → dispute_raised |
+4989000000008 |
callback_requested and do_not_call_requested |
+4989000000009 |
Agent ends the call → actor: agent, reason: abusive |
| anything else | A plausible outcome, seeded from the order ID - reproducible |
That last row is useful: the same order ID always produces the same outcome, so a fixture set built from your own real numbers behaves consistently across runs.
The simulate block
Section titled “The simulate block”For anything the magic numbers do not cover:
{ "order_type": "zahlungserinnerung", "subject": { "…": "…" }, "contacts": [{ "channel": "phone", "value": "+4915112345678" }], "context": { "…": "…" }, "simulate": { "dispositions": ["no_answer", "no_answer", "answered"], "outcome": "installment_plan_requested", "delay_seconds": 5 }}Three capabilities, and a clear division of labour between the first two:
dispositions decides whether anyone picks up. One entry per dial attempt,
in order. If the list is shorter than max_attempts, the last entry repeats -
otherwise a single-element list would carry no sensible meaning.
outcome decides how the conversation ended. It must be a real conversation
outcome; an invented string is rejected with 422 rather than normalised into
summary_code: other, which would be an honest but useless result for someone trying
to reproduce a specific case.
delay_seconds delays the result. Capped at 600 s, because the platform declares
an attempt stuck after 30 minutes - a longer delay would hand you a platform error
instead of the behaviour you ordered.
The block is typed rather than free-form. A typo such as dispostions is a 422 at
submission instead of a call that behaves strangely - in an environment that exists
solely for trying things out, a silent misfire is the wrong failure mode.
What the sandbox does not fake
Section titled “What the sandbox does not fake”The simulator does not fabricate results. It writes exactly the records a real call writes on hang-up, then runs the same derivation over them that production runs. There is no second result producer.
This matters more than it sounds. A simulator that assembled Result objects would
only ever test the simulator. This one exercises the live derivation - and on its
first run it surfaced three defects in the production path, including one where
every completed conversation would have been reported as “not reached”.
For you, the consequence is simple: a result shaped in the sandbox is shaped the same way live. Writing your handler against sandbox output is not throwaway work.
Differences from production
Section titled “Differences from production”The first attempt lands on the next quarter-hour. Dial instants sit on a fifteen-minute grid. An order submitted at 09:07 is not attempted before 09:15. Build that into test scripts rather than treating it as a hang.
busy is not offered. It is in the CallDisposition registry, but the live path
maps a busy signal onto no_answer and never produces the value. A sandbox that
handed you busy would invite retry_on: ["busy"] - green in test, dead in
production. The value is withheld rather than promised.
Transcripts exist in German and English only. The platform also speaks Czech, but inventing a Czech transcript would be asserting a translation quality nobody has checked. A German transcript in a Czech tenant is obvious when you look at it; a bad Czech one is not.
There are no test clocks. You cannot fast-forward a retry window or a wait step. Test long waits with short durations.
A test matrix worth building
Section titled “A test matrix worth building”Nine cases that between them exercise every branch a production integration needs:
-
The happy path - …001
A full result with
outcomes[]populated. Your primary handler. -
Nobody home - …004
not_reachedafter retries. Confirms your code does not assumeresult.identityis present. -
Wrong person - …006
wrong_party. Should route to data correction, not to collections follow-up. -
A dispute - …007
dispute_raised. Usually needs to stop further automated contact on your side. -
An objection - …008
Two outcomes on one call, and a suppression that exists before you see the result. Verify you do not schedule another order for that subject.
-
Suppressed on submission
Create a suppression, then submit an order for the same subject. Expect 422
subject_suppressed- and check that your code treats it as an expected outcome rather than an incident. -
A timeout retry
Submit twice with the same
Idempotency-Key. Expect the same order ID both times. -
A duplicate signal
Emit the same
dedupe_keytwice. Expectduplicate: trueand no second effect. -
An unknown enum value
Feed your handler a
summary_codeyou do not recognise and confirm it degrades to “other” instead of throwing. New values arrive without a version bump.
Checking the plumbing without waiting
Section titled “Checking the plumbing without waiting”POST /v1/webhook-endpoints/{id}/test sends a ping immediately. Use it to confirm
signature verification, TLS and routing before you submit anything - it is much
faster than discovering a signature bug fifteen minutes into a scheduled dial.
ping carries sequence: null, since it belongs to no aggregate. If your handler
requires a sequence number, this is where you find out.