Quickstart
By the end of this page you will have placed a simulated call to a magic test
number and received a call_order.completed result. No real telephone rings at
any point: the sandbox host runs simulated telephony.
1. Get a token
Section titled “1. Get a token”Tokens come from the authorisation server, not from the API host. They are valid for 30 minutes and there are no refresh tokens: when a token expires you request another with the same credentials. Ask for a token slightly before you need one rather than retrying on the first 401.
curl -sS -X POST https://auth.pacepayments.ai/oauth2/token \-d grant_type=client_credentials \-d client_id="$PACE_CLIENT_ID" \-d client_secret="$PACE_CLIENT_SECRET"import os, requests
token = requests.post("https://auth.pacepayments.ai/oauth2/token",data={ "grant_type": "client_credentials", "client_id": os.environ["PACE_CLIENT_ID"], "client_secret": os.environ["PACE_CLIENT_SECRET"],},timeout=15,).json()
access_token = token["access_token"] # valid for token["expires_in"] secondsconst res = await fetch("https://auth.pacepayments.ai/oauth2/token", {method: "POST",headers: { "Content-Type": "application/x-www-form-urlencoded" },body: new URLSearchParams({grant_type: "client_credentials",client_id: process.env.PACE_CLIENT_ID!,client_secret: process.env.PACE_CLIENT_SECRET!,}),});
const { access_token, expires_in } = await res.json();using System.Net.Http;
var http = new HttpClient();var res = await http.PostAsync("https://auth.pacepayments.ai/oauth2/token",new FormUrlEncodedContent(new Dictionary<string, string>{ ["grant_type"] = "client_credentials", ["client_id"] = Environment.GetEnvironmentVariable("PACE_CLIENT_ID"), ["client_secret"] = Environment.GetEnvironmentVariable("PACE_CLIENT_SECRET"),}));
using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());var accessToken = doc.RootElement.GetProperty("access_token").GetString();The response carries the scopes actually granted to your registration. Check them
once during setup - a missing scope shows up later as a 403 insufficient_scope
on one endpoint while everything else works.
2. Find an order type
Section titled “2. Find an order type”An order type is the purpose of the call. It binds the conversation to agent behaviour vetted by Pace, which is why you reference one instead of describing what the agent should say.
curl -sS https://sandbox.api.pacepayments.ai/v1/order-types \-H "Authorization: Bearer $ACCESS_TOKEN"types = requests.get("https://sandbox.api.pacepayments.ai/v1/order-types",headers={"Authorization": f"Bearer {access_token}"},timeout=15,).json()["data"]
key = next(t["key"] for t in types if t["status"] == "active")const list = await fetch("https://sandbox.api.pacepayments.ai/v1/order-types",{ headers: { Authorization: `Bearer ${access_token}` } },).then((r) => r.json());
const key = list.data.find((t: any) => t.status === "active").key;http.DefaultRequestHeaders.Authorization =new AuthenticationHeaderValue("Bearer", accessToken);
var types = await http.GetFromJsonAsync<JsonElement>("https://sandbox.api.pacepayments.ai/v1/order-types");
var key = types.GetProperty("data").EnumerateArray() .First(t => t.GetProperty("status").GetString() == "active") .GetProperty("key").GetString();Keys are German - zahlungserinnerung, mahnung, forderungsuebernahme and so on.
They are identifiers, not display text; read title for something to show a user.
3. Submit a call order
Section titled “3. Submit a call order”Use the magic number +4989000000001. In the sandbox it produces a conversation
that ends in a payment promise, which gives you a fully populated result to write
your handler against.
curl -sS -X POST https://sandbox.api.pacepayments.ai/v1/call-orders \-H "Authorization: Bearer $ACCESS_TOKEN" \-H "Content-Type: application/json" \-H "Idempotency-Key: $(uuidgen)" \-d '{"order_type": "zahlungserinnerung","client_reference": "INV-2026-118442","subject": { "reference": "CUST-88213", "name": { "given_name": "Erika", "family_name": "Mustermann" }, "date_of_birth": "1989-03-28", "address": { "street": "Seminarstr.", "house_number": "2b", "postal_code": "01067", "city": "Dresden", "country": "DE" }, "timezone": "Europe/Berlin"},"contacts": [ { "channel": "phone", "value": "+4989000000001", "type": "mobile", "priority": 1 }],"context": { "claim": { "outstanding": { "value": "129.90", "currency": "EUR" }, "creditor_name": "Truman Textilien GmbH", "basis": { "kind": "subscription", "description": "Annual textile care plan" } }}}'import uuid
order = {"order_type": key,"client_reference": "INV-2026-118442","subject": { "reference": "CUST-88213", "name": {"given_name": "Erika", "family_name": "Mustermann"}, "date_of_birth": "1989-03-28", "address": { "street": "Seminarstr.", "house_number": "2b", "postal_code": "01067", "city": "Dresden", "country": "DE", }, "timezone": "Europe/Berlin",},"contacts": [ {"channel": "phone", "value": "+4989000000001", "type": "mobile", "priority": 1},],"context": { "claim": { "outstanding": {"value": "129.90", "currency": "EUR"}, "creditor_name": "Truman Textilien GmbH", "basis": {"kind": "subscription", "description": "Annual textile care plan"}, },},}
created = requests.post("https://sandbox.api.pacepayments.ai/v1/call-orders",headers={ "Authorization": f"Bearer {access_token}", "Idempotency-Key": str(uuid.uuid4()),},json=order,timeout=15,).json()
order_id = created["id"] # ord_… - status is "accepted"const order = {order_type: key,client_reference: "INV-2026-118442",subject: {reference: "CUST-88213",name: { given_name: "Erika", family_name: "Mustermann" },date_of_birth: "1989-03-28",address: { street: "Seminarstr.", house_number: "2b", postal_code: "01067", city: "Dresden", country: "DE",},timezone: "Europe/Berlin",},contacts: [{ channel: "phone", value: "+4989000000001", type: "mobile", priority: 1 },],context: {claim: { outstanding: { value: "129.90", currency: "EUR" }, creditor_name: "Truman Textilien GmbH", basis: { kind: "subscription", description: "Annual textile care plan" },},},};
const created = await fetch("https://sandbox.api.pacepayments.ai/v1/call-orders",{method: "POST",headers: { Authorization: `Bearer ${access_token}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(),},body: JSON.stringify(order),},).then((r) => r.json());
const orderId = created.id;var order = new{order_type = key,client_reference = "INV-2026-118442",subject = new{ reference = "CUST-88213", name = new { given_name = "Erika", family_name = "Mustermann" }, date_of_birth = "1989-03-28", address = new { street = "Seminarstr.", house_number = "2b", postal_code = "01067", city = "Dresden", country = "DE", }, timezone = "Europe/Berlin",},contacts = new[]{ new { channel = "phone", value = "+4989000000001", type = "mobile", priority = 1 },},context = new{ claim = new { outstanding = new { value = "129.90", currency = "EUR" }, creditor_name = "Truman Textilien GmbH", basis = new { kind = "subscription", description = "Annual textile care plan" }, },},};
var req = new HttpRequestMessage(HttpMethod.Post,"https://sandbox.api.pacepayments.ai/v1/call-orders"){Content = JsonContent.Create(order),};req.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var created = await (await http.SendAsync(req)).Content.ReadFromJsonAsync<JsonElement>();
var orderId = created.GetProperty("id").GetString();You get 202 Accepted and an order in status accepted. Acceptance means the
order passed validation, the order type exists, the subject is not on the
suppression list, and a permissible dial instant exists inside your window. It does
not mean anyone has been called yet.
4. Collect the result
Section titled “4. Collect the result”Two routes, and they carry the same events. Start with the feed - it needs no public endpoint, so you can finish this quickstart from a laptop.
The feed retains 30 days. after accepts either a cursor from a previous page
or a bare event ID, so you can resume from the last event you processed.
curl -sS "https://sandbox.api.pacepayments.ai/v1/events?event_types=call_order.*" \ -H "Authorization: Bearer $ACCESS_TOKEN"events = requests.get( "https://sandbox.api.pacepayments.ai/v1/events", headers={"Authorization": f"Bearer {access_token}"}, params={"call_order_id": order_id}, timeout=15,).json()["data"]
done = next( (e for e in events if e["type"] == "call_order.completed"), None)Register the endpoint, then echo the challenge back. Until you do, events accumulate in the feed and nothing is delivered - the challenge is how Pace establishes that somebody who may receive customer data is actually listening.
curl -sS -X POST https://sandbox.api.pacepayments.ai/v1/webhook-endpoints \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.org/pace/v1", "event_types": ["call_order.*"] }'The response carries secret once. Store it now; it is never retrievable
again. Then read Receiving results for signature
verification before you accept anything in production.
5. Read the result
Section titled “5. Read the result”Fetch the order once it reaches a terminal status. result is present on every
terminal order, including ones where nobody answered.
curl -sS "https://sandbox.api.pacepayments.ai/v1/call-orders/$ORDER_ID" \ -H "Authorization: Bearer $ACCESS_TOKEN"{ "id": "ord_01J32K7M2QW9RT", "status": "completed", "client_reference": "INV-2026-118442", "order_type_version": 1, "decisive_call_id": "call_01J32K8P4R…", "result": { "reachability": "reached", "engagement": "conversation", "identity": { "verified": true, "method": "address_birthday" }, "termination": { "actor": "subject", "reason": "completed" }, "summary_code": "payment_promised", "outcomes": [ { "type": "payment_promise", "data": { "amount": { "value": "129.90", "currency": "EUR" }, "due_date": "2026-09-15" } } ] }, "compliance": { "ai_disclosure": { "disclosed": true, "at": "2026-08-30T09:14:22+02:00" }, "contact_window_policy": "de_uwg7_default" }}Branch on summary_code. It is one value, deliberately, so that a client has one
thing to switch on - the four dimensions above it explain why the call came out
that way, and outcomes[] carries the structured detail.