Sandbox Test Scenario Matrix
This guide helps customers and partners test business scenarios against the Amili REST API in the sandbox environment while building an integration. Each scenario shows how to trigger it (the REST calls you make) and how to verify it (what to check in the response).
It is organised by use case — status updates (reminder, debt collection, payments) and payout information — and complements the Use Case Matrix, which describes what to integrate rather than how to verify it.
Before you start
Access and endpoints
Sandbox access is provisioned by Amili, not self-service. During onboarding Amili registers your public key, issues your API code, and creates the account(s) and creditor(s) you test against.
- API base:
https://api-sandbox.amili.se(interactive reference at/docs) - Creditor app:
https://app-sandbox.amili.se· Customer app:https://ada-customer-sandbox.vfs.visma.com - Auth: sign a short-lived JWT with your private key →
GET /authenticates/api-code→ use the returned token asX-API-Keyon every request. See Authentication.
All examples below assume a valid X-API-Key.
Your setup (configured by Amili)
Your account, creditor and agreement are set up by Amili during onboarding — you don't configure these yourself. Two of these settings shape what you can test, so they're worth knowing for context:
- Your agreement determines which actions a case can start with —
reminderand/ordebt_collection— and whether you can register invoices directly (full-service). These differ from one agreement to another. - Your creditor must be active, and its
payout_details(bank account) are where payouts are sent.
You can read your creditor with GET /creditors to see its current settings. If a call is rejected because a setting isn't enabled for your creditor, contact your Amili integration contact.
Sandbox vs your production creditor
Sandbox behaves like production at the API level. What can differ is your production creditor's own configuration (fees, payout intervals, reminder timing) — it's set up from your agreement and may not match the generic sandbox test creditor. Build against the API contract rather than specific sandbox values, and confirm production-specific values with your Amili contact. Environment behaviour also differs — see Sandbox limitations below.
How transitions happen in sandbox
Worth understanding before testing status updates:
- The standard flow is time-driven — an automated process picks up due cases roughly every 10 minutes; there is no time acceleration. When the first reminder fires depends on the invoice's due date: an invoice that is already overdue at registration has its reminder scheduled for the next flow run (it goes out the same day, ~an hour later), while an invoice due in the future waits until around its due date. Each later step — e.g. reminder → debt collection — then waits the reminder letter's payment window, usually ~10 banking days (domestic) plus a couple of banking days, so those steps take real calendar time.
- Forward progression has no public fast-forward. There is no API call that advances a case to its next stage on demand; if you need a case moved forward without waiting, that is an Amili-side action.
- Some transitions can be triggered on demand via domain actions rather than waiting for the flow — e.g.
respiteandclose_case(under/domain-actions/cases/...). Forward progression (reminder → debt collection) is not among them. - Payments must be simulated. Sandbox has no real banking. To make money settle against a case (and produce a payout) you use a demo payment:
Sandbox processes these on a fixed cadence (times UTC). None accelerate time — they act on items whose scheduled date has already passed:
| Process | Sandbox cadence | What it does |
|---|---|---|
| Flow progression | ~every 10 min | Sends reminders and escalates reminder → debt collection when due |
| Payment settlement | ~hourly | Settles demo payments → closes the case → creates finance--payout-specifications |
| Payout aggregation | daily (~04:30) | Groups payout specifications into finance--payouts, per the creditor's payout interval |
A creditor payment is not a payout
POST /creditor--payments records money the creditor already received directly. It closes a fully-paid case with reason capital_paid_to_creditor but generates no payout. To test payout information, use a demo payment (debtor → Amili), not a creditor payment.
Scenario matrix
1. Connectivity
| Scenario | How to trigger | How to verify |
|---|---|---|
| Authenticate to sandbox | GET /authenticates/api-code with a JWT signed by your private key | 200 with { token }; the token works as X-API-Key |
| Confirm your creditor is ready | GET /creditors/{creditor_id} | is_active: true; payout_details present (agreement via GET /agreements) |
2. Register a claim
Which first_action values are available — and whether you can register invoices directly — depends on your agreement. Follow the part that matches the claim type you register.
Which test customers to use
Use a real Swedish organisation number — e.g. a kommun or any registered company. Organisation numbers are public information.
Avoid made-up identity numbers. Registration itself returns 201, but the customer is resolved in a following step — if the identity number can't be resolved that step fails with Failed to initialize customer and the case never starts.
The case does not start immediately
Registration returns 201 straight away, but the case is created with state: "initializing" and status: "initializing". The customer is resolved and the flow started by a following step that runs every 10 minutes — expect up to ~10 minutes before the case reaches its real state. The verifications below assume that step has run.
2A. Register an invoice (full-service)
| Scenario | How to trigger | How to verify |
|---|---|---|
| Register an invoice | POST /invoice--registrations (creditor, customer, matrix, invoice_date, invoice_due_date, currency) | 201 → response has _invoice, _case, _ocr_number, ocr_number_ocr |
2B. Register a case for a reminder (PM)
| Scenario | How to trigger | How to verify |
|---|---|---|
| Register a case for a reminder | POST /case--registrations with first_action: "reminder" (plus creditor, currency, customer, debts[]) | 201 → _cases[0], _ocr_numbers[0], _ocr_numbers_ocr[0].GET /cases/{_cases[0]} → state: "reminder" (once initialization has run) |
2C. Register a case to debt collection (IK)
| Scenario | How to trigger | How to verify |
|---|---|---|
| Register a case to debt collection | POST /case--registrations with first_action: "debt_collection" (plus creditor, currency, customer, debts[]) | 201 → _cases[0].GET /cases/{id} → case is in the debt-collection track (once initialization has run) |
Common to all claim types
| Scenario | How to trigger | How to verify |
|---|---|---|
| Attach the original invoice PDF | 1. Register a case (above) → _cases[0]2. POST /media--upload/{case_id} (multipart: file, domain: cases, dotted_path: original_invoice) | 200 with { url } (signed link) |
3. Status updates — reminder & escalation
Getting the first reminder out quickly
The first reminder is scheduled from the invoice's due date, not a fixed delay. Register the case with an invoice date / due date already in the past and the reminder is queued for the next flow run (same day) instead of waiting. (first_action_date can delay the first action but can't bring it earlier than the due date.)
| Scenario | How to trigger | How to verify |
|---|---|---|
| Case reaches the reminder stage | 1. POST /case--registrations (first_action: "reminder") → _cases[0]2. (automatic) the flow progresses it | GET /cases/{id} → state: "reminder", status: "reminder" (or "soft_reminder" early); next_flow_item.schedule_to_execute is set |
| Escalate reminder → debt collection | 1. Register a reminder case (above) 2. Let real calendar days pass (usually ~10 banking days), or ask Amili to advance | GET /cases/{id} → state: "debt_collection", status: "debt_collection"; state_history[] shows the transition |
| Read current status on demand | GET /cases/{id}?projection={"state":1,"status":1,"closed":1,"total_remaining_capital_amount":1} | Response reflects live state, status, remaining capital |
| Read the transition history | GET /cases/{id} (with state_history, status_history)GET /case--histories?where={"cases.case":"<id>"} | state_history[] / status_history[] entries; case--histories activity log |
Filtering case--histories by case
On case--histories the cases field is a list of objects, so filter on the nested reference — where={"cases.case":"<case_id>"}. Filtering on cases directly returns nothing.
Closure does not change state
On close, the platform sets status: "closed" and a closed sub-document { reason, close_date } but leaves state unchanged. Detect closure via status == "closed" and/or the presence of closed, not via state.
4. Status updates — creditor actions on a case
Each of these actions settles synchronously in the POST and returns a per-item status (completed / warning / error).
| Scenario | How to trigger | How to verify |
|---|---|---|
| Register a full payment (paid to creditor) | 1. Register a case (§2) → _cases[0]2. POST /creditor--payments { creditor, case, amount, currency, bank_transaction_date, origin } (full balance) | _payment_status: "completed".GET /cases/{id} → total_remaining_capital_amount: 0, status: "closed", closed.reason: "capital_paid_to_creditor" |
| Register a partial payment | 1. Register a case (§2) 2. POST /creditor--payments with part of the balance | _payment_status: "completed".GET /cases/{id} → total_remaining_capital_amount reduced, no closed |
| Credit part of the capital | 1. Register a case (§2) 2. POST /creditor--creditings { case, amount, currency, origin } | _crediting_status: "completed".GET /cases/{id} → capital reduced; if zeroed → closed.reason: "credit" |
| Cancel a case | 1. Register a case (§2) 2. POST /creditor--cancellations { creditor, case, origin } | _cancellation_status: "completed".GET /cases/{id} → status: "closed", closed.reason: "cancellation" |
| Grant a respite (postpone) | 1. Register a case with a pending next step (§2) 2. GET /domain-actions/cases/respite/{id} (200 = allowed, 206 = issues)3. POST /domain-actions/cases/respite/{id} { days: 14 } | POST returns 200.GET /cases/{id} → next_flow_item.schedule_to_execute pushed out; state/status unchanged (see note) |
Respite availability
Respite is available up to the point a case is handed over to enforcement (Kronofogden) — including while in reminder, debt collection and debt surveillance. The maximum is usually 30 days per respite, and the number of respites is effectively unlimited in sandbox (these limits are configured per creditor).
5. Payments & payouts (demo payment)
POST /demo--payments exists only in sandbox — it simulates an incoming bank payment and is called with your normal token.
A demo payment is addressed by OCR record, not by case. So pick the case you want to pay and look up its ocr_number_id first:
| You have | Get ocr_number_id from |
|---|---|
| The registration response (§2) | Invoice: _ocr_number · Case: _ocr_numbers[n] — same index as _cases[n] |
| Only a case id | GET /ocr-numbers?where={"case.case":"<case_id>"} → _items[0]._id |
| An OCR record id | GET /ocr-numbers/{id} — the OCR string itself is the ocr field |
The /ocr-numbers resource is read-only. Registration also returns the OCR strings directly (ocr_number_ocr / _ocr_numbers_ocr[]).
Pay the full balance, not just the capital
A case only closes as fully paid when the sum of all its transactions reaches zero — capital, fees and interest. Read the full balance from total_remaining_transaction_amount on the case and use that as amount. total_remaining_capital_amount is the capital portion only; paying that leaves the case partially paid and it will not close.
| Scenario | How to trigger | How to verify |
|---|---|---|
| Simulate a debtor payment to Amili | 1. Look up ocr_number_id for the case (above)2. POST /demo--payments { amount, ocr_number_id } | 200 with an empty body → a queued bank credit is created for the case |
| Case settles & closes as paid | Automatic (settlement runs ~hourly) | GET /cases/{id} → total_remaining_capital_amount: 0, status: "closed", closed.reason in the fully_paid* family |
| Payout specification created | Automatic on settlement | GET /finance--payout-specifications?where={"references.case":"<id>"} → rows with finance_category (capital, creditor_outlay), amount, booking_date |
| Aggregated payout created | Automatic (aggregation runs daily) | GET /finance--payouts?where={"creditor":"<id>"} → summary rows grouped by finance_category / period |
| Track revenue (interest / commission) | GET /finance--payouts?where={"creditor":"<id>","finance_category":{"$in":["interest","creditor_commission"]}} | Summary amounts per category |
Payout timing in sandbox
finance--payout-specifications appear within ~an hour of the demo payment (settlement). The aggregated finance--payouts only appear once the creditor's payout_interval period closes and on a banking day. In sandbox the intervals are usually capital and creditor_outlay = daily (a capital payout appears the next banking day), interest = monthly, and creditor_commission = quarterly — so capital settles quickly while interest and commission lag.
6. Tracking & reconciliation
| Scenario | How to trigger | How to verify |
|---|---|---|
| Export open cases | GET /cases?where={"closed":{"$exists":false}}&projection={"state":1,"status":1,"total_remaining_capital_amount":1}&max_results=50 | _items[] with live state/status/remaining; _meta.total |
| Export closed cases | GET /cases?where={"closed.close_date":{"$gt":"Mon, 13 Jan 2026 00:00:00 GMT"}}&sort=-closed.close_date | _items[] with closed.reason (paid vs. loss) |
| Correlate a payout back to a case | GET /finance--payout-specifications?where={"references.case":"<id>"} | invoice_number, reference_number, booking_date, references.case |
Sandbox limitations — what you cannot fully verify
| Area | Behaviour in sandbox |
|---|---|
| Letters | Generated but not physically sent |
| Not delivered | |
| Enforcement (Kronofogden) | A case can enter the enforcement state, but nothing is exported to the authority |
| Banking / payouts | No real payments; payout files run in test mode (validated, not executed) |
Related documentation
- Use Case Matrix — what to integrate
- Environments — sandbox vs. production
- Authentication — obtaining and using tokens
- Invoice · Case — registration bodies
- Case Status Tracking — states, statuses, close reasons
- Creditor Case Actions — payments, credits, cancellations, respite
- Finance & Payouts — payout specifications and payouts
