Skip to content

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.ai

Sandbox credentials are separate from live ones. See Environments.

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_paiddispute_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.

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.

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.

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.

Nine cases that between them exercise every branch a production integration needs:

  1. The happy path - …001

    A full result with outcomes[] populated. Your primary handler.

  2. Nobody home - …004

    not_reached after retries. Confirms your code does not assume result.identity is present.

  3. Wrong person - …006

    wrong_party. Should route to data correction, not to collections follow-up.

  4. A dispute - …007

    dispute_raised. Usually needs to stop further automated contact on your side.

  5. 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.

  6. 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.

  7. A timeout retry

    Submit twice with the same Idempotency-Key. Expect the same order ID both times.

  8. A duplicate signal

    Emit the same dedupe_key twice. Expect duplicate: true and no second effect.

  9. An unknown enum value

    Feed your handler a summary_code you do not recognise and confirm it degrades to “other” instead of throwing. New values arrive without a version bump.

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.