Skip to content

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 as X-API-Key on 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 — reminder and/or debt_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. respite and close_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:

ProcessSandbox cadenceWhat it does
Flow progression~every 10 minSends reminders and escalates reminder → debt collection when due
Payment settlement~hourlySettles demo payments → closes the case → creates finance--payout-specifications
Payout aggregationdaily (~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

ScenarioHow to triggerHow to verify
Authenticate to sandboxGET /authenticates/api-code with a JWT signed by your private key200 with { token }; the token works as X-API-Key
Confirm your creditor is readyGET /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)

ScenarioHow to triggerHow to verify
Register an invoicePOST /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)

ScenarioHow to triggerHow to verify
Register a case for a reminderPOST /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)

ScenarioHow to triggerHow to verify
Register a case to debt collectionPOST /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

ScenarioHow to triggerHow to verify
Attach the original invoice PDF1. 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.)

ScenarioHow to triggerHow to verify
Case reaches the reminder stage1. 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 collection1. 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 demandGET /cases/{id}?projection={"state":1,"status":1,"closed":1,"total_remaining_capital_amount":1}Response reflects live state, status, remaining capital
Read the transition historyGET /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).

ScenarioHow to triggerHow 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 payment1. 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 capital1. 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 case1. 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 haveGet ocr_number_id from
The registration response (§2)Invoice: _ocr_number · Case: _ocr_numbers[n] — same index as _cases[n]
Only a case idGET /ocr-numbers?where={"case.case":"<case_id>"}_items[0]._id
An OCR record idGET /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.

ScenarioHow to triggerHow to verify
Simulate a debtor payment to Amili1. 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 paidAutomatic (settlement runs ~hourly)GET /cases/{id}total_remaining_capital_amount: 0, status: "closed", closed.reason in the fully_paid* family
Payout specification createdAutomatic on settlementGET /finance--payout-specifications?where={"references.case":"<id>"} → rows with finance_category (capital, creditor_outlay), amount, booking_date
Aggregated payout createdAutomatic (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

ScenarioHow to triggerHow to verify
Export open casesGET /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 casesGET /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 caseGET /finance--payout-specifications?where={"references.case":"<id>"}invoice_number, reference_number, booking_date, references.case

Sandbox limitations — what you cannot fully verify

AreaBehaviour in sandbox
LettersGenerated but not physically sent
EmailNot delivered
Enforcement (Kronofogden)A case can enter the enforcement state, but nothing is exported to the authority
Banking / payoutsNo real payments; payout files run in test mode (validated, not executed)