> ## Documentation Index
> Fetch the complete documentation index at: https://docs.paypathiq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Test every outcome in sandbox

> Real requests that produce each success, conflict and hold outcome in the sandbox, using only your own synthetic data.

This page shows how to make the sandbox return each outcome you need to handle. Every request here is an ordinary API request with data you choose. Each result comes from the API's rules, which are linked to the matching [problem page](/problems/validation_error) or concept.

<Warning>
  There are no special test IDs, magic amounts or trigger values in the sandbox yet. No particular `externalCustomerId`, amount or `sourceReference` makes the API fail on purpose. You produce each outcome by sending a request that really breaks the rule, as shown below.
</Warning>

## Before you start

* Use a sandbox key and your `clientCode`. See [Get a sandbox key](/authentication#get-a-sandbox-key).
* Make your IDs unique for each run, for example by adding a run number: `REC_A_RUN7`. The API has no delete operation and no sandbox reset, so a repeat of an ID is a replay of the earlier record. The [certification harness](/certification#run-the-harness) does the same.
* Send requests one at a time. A key allows one request in flight, so a second concurrent request returns [`429 rate_limit_exceeded`](/problems/rate_limit_exceeded).
* **Feed mode** matters. A connection is either `transaction` (the default) or `snapshot_only`. It is a setting on your connection, not a field you send. PayPathIQ sets it. If you need the other mode, ask your PayPathIQ Integration administrator. See [Source balance snapshots](/concepts#source-balance-snapshots).

The examples use two Receivables you create first, following the [sandbox quickstart](/quickstart) or the [credit union guide](/credit-union): `REC_A` for EUR 1,000.00 and `REC_B` for EUR 400.00, both at `sourceRevision` 1, for one Customer `CUS_1`.

## Replays and conflicts

An identical repeat is a safe replay and returns `200`. The same ID with different content is refused. Nothing is saved when a request is refused.

| To see | Send | You get |
| - | - | - |
| A safe replay | Send the same `PUT` for `REC_A` twice, byte for byte the same content | `201` the first time, `200` the second. Nothing changes. |
| A conflicting replay of a Receivable | `PUT` `REC_A` again at `sourceRevision` 1 with a different `recoverableAmount` | [`409 source_revision_conflict`](/problems/source_revision_conflict). A changed record needs a newer `sourceRevision`. |
| A balance change disguised as an update | `PUT` `REC_A` with `sourceRevision` 2 and a different `recoverableAmount` | [`409 financial_operation_required`](/problems/financial_operation_required). A descriptive update cannot replace a balance. Use a payment, a correction or a snapshot. |
| A conflicting replay of a Payment | `PUT` an existing `externalPaymentObservationId` with a different `amount` | [`409 payment_observation_conflict`](/problems/payment_observation_conflict) |
| A conflicting replay of a correction or snapshot | Reuse an `externalEventId` or `externalSnapshotId` with different content | [`409 financial_event_conflict`](/problems/financial_event_conflict) |
| Wrong media type | `PATCH` a Customer or Receivable with `application/json` | `415 unsupported_media_type` |
| An unknown field | Add `"unknownField": 1` to any body | [`422 validation_error`](/problems/validation_error) with an `errors` list. See [field error codes](/errors-and-retries#field-error-codes). |
| Missing, unknown or out-of-scope key | Omit the `Authorization` header, send a made-up key, or use another Client's code | `401 invalid_api_key` for the first two, [`403 client_scope_denied`](/problems/client_scope_denied) for the last |

## Payments and allocations (transaction mode)

Allocation means saying which Receivable a payment pays. Money you do not allocate stays unapplied, which means it reduces no balance. See [Payment observations](/concepts#payment-observations).

<Steps>
  <Step title="Allocate part of a payment">
    `PUT /v1/clients/{clientCode}/payment-observations/PAY_1`:

    ```json theme={null}
    {
      "type": "payment",
      "amount": { "amount": "300.00", "currency": "EUR" },
      "allocations": [
        { "externalReceivableId": "REC_A", "amount": { "amount": "250.00", "currency": "EUR" } }
      ],
      "externalCustomerId": "CUS_1",
      "method": "bank_transfer",
      "effectiveAt": "2026-10-14T10:00:00Z",
      "sourceReference": "BANK_1"
    }
    ```

    You get `201` with `unappliedAmount` of EUR 50.00. `REC_A` now reads EUR 750.00.
  </Step>

  <Step title="Allocate more than the balance">
    A payment of EUR 500.00 allocating EUR 500.00 to `REC_B` (balance EUR 400.00) returns [`409 allocation_exceeds_balance`](/problems/allocation_exceeds_balance). The payment itself is not saved either.
  </Step>

  <Step title="Refund more than was allocated">
    A `refund` with `relatedExternalPaymentObservationId` set to `PAY_1` and an amount and allocation of EUR 260.00 to `REC_A` returns `409 payment_observation_conflict`. The total of corrections cannot exceed the EUR 250.00 the payment allocated to `REC_A`.
  </Step>

  <Step title="Refund part of it">
    A `refund` of EUR 50.00 with an allocation of EUR 40.00 to `REC_A` returns `201`. EUR 10.00 is unapplied from the refund. `REC_A` reads EUR 790.00.
  </Step>
</Steps>

In `snapshot_only` mode every one of these event writes (payments, refunds, reversals, credit notes, adjustments, write-offs) returns [`409 financial_operation_required`](/problems/financial_operation_required). That is the outcome to test in that mode.

## Balance snapshots

A snapshot is an ordered set of Source balances. `sourcePosition` must rise with every snapshot you send, and `asOf` is a UTC time that is not in the future. Send `PUT /v1/clients/{clientCode}/balance-snapshots/SNAP_n`.

| To see | Mode | Send | You get |
| - | - | - | - |
| `checkpoint` | transaction | A snapshot with `REC_A`'s **current** balance and `sourceRevision`, `asOf` set to now | `201`, line outcome `checkpoint`. The Source agrees with the ledger. |
| `held` (`source_balance_mismatch`) | transaction | The same, but a different `recoverableAmount` | `201`, line outcome `held`, `holdReason` `source_balance_mismatch`. The Receivable read shows `financialPosition.uncertainty`, and affected recovery and payment actions are held. A later `checkpoint` that matches clears it. |
| `applied` | snapshot-only | A higher `sourcePosition`, a newer `sourceRevision` and a new `recoverableAmount` | `201`, line outcome `applied`. The change is recorded as a ledger adjustment, never as a Payment. |
| `unchanged` | snapshot-only | A higher `sourcePosition` with the **same** `sourceRevision` and amount as the Receivable already has | `201`, line outcome `unchanged` |
| A stale snapshot | either | A `sourcePosition` equal to or lower than any accepted snapshot | [`409 source_position_conflict`](/problems/source_position_conflict) |
| A stale `asOf` | either | A higher `sourcePosition` but an `asOf` earlier than the latest accepted snapshot | `409 source_position_conflict` |
| An old revision | either | A line whose `sourceRevision` is below the Receivable's accepted revision | `409 source_revision_conflict` |
| The same revision, a different balance | snapshot-only | The Receivable's current `sourceRevision` with a different amount | `409 source_revision_conflict` |
| An unknown Receivable | either | A line for an ID you never created | [`409 dependency_missing`](/problems/dependency_missing). One bad line rejects the whole snapshot. |
| A future `asOf` | either | `asOf` more than 5 minutes ahead of the current time | `422 validation_error` with the pointer `/asOf` |
| Duplicate lines | either | The same `externalReceivableId` twice | `422 validation_error` |
| An identical retry | either | The same snapshot ID and body again | `200`, nothing changes |

`pending_payment_overlap_unknown` is the other hold reason. It needs a Payment that a payment provider confirmed, with an effective time at or before the snapshot's `asOf`, that the Source has not yet acknowledged. A hosted payment is confirmed only by a verified provider event, which an API call cannot fake. So you cannot make this hold from the API alone. Test your handling of `held` with `source_balance_mismatch`, and treat any `held` outcome as a hold, whatever `holdReason` says.

## Change feed and webhooks

| To see | Send | You get |
| - | - | - |
| A fresh cursor | `GET .../changes?from=latest` | `200`, empty `data`, a `nextCursor` |
| An unusable cursor | `GET .../changes?cursor=not-a-cursor` | [`422 invalid_change_cursor`](/problems/invalid_change_cursor) |
| Both parameters | `GET .../changes?cursor=...&from=latest` | `422 validation_error` |
| Page size out of range | `GET .../changes?limit=501` | `422 validation_error` |
| An expired cursor | Not reproducible on demand. Changes are kept 30 days, so a cursor expires only after that. | [`410 change_cursor_expired`](/problems/change_cursor_expired). Test your recovery with the polling loop's resynchronize function, calling it directly. |
| An unsafe webhook URL | `PUT .../webhook-endpoint` with `http://hooks.example.com/x`, a URL with a port other than 443, an IP address, or `https://localhost/x` | [`422 webhook_destination_unsafe`](/problems/webhook_destination_unsafe) |
| A signed delivery | Configure a real public HTTPS endpoint, then change a Receivable | A signed `POST` within about a minute. There is no test-event endpoint; use the [test vector](/webhooks#test-vector) for offline checks. |

## Other outcomes

| To see | Send | You get |
| - | - | - |
| A stale payment request | `PUT .../payment-requests/PR_1` for an amount that is not `REC_B`'s current balance | [`409 payment_request_stale`](/problems/payment_request_stale) |
| A changed payment request | Resend `PR_1` with a different body | [`409 payment_request_conflict`](/problems/payment_request_conflict) |
| A blocked batch record | Submit a batch of a Receivable for a Customer that does not exist, and a second record whose `dependsOn` names that Receivable record | The first record is `failed` with `dependency_missing`; the second is `dependency_failed` with `blockedBy`. See [Asynchronous batches](/concepts#asynchronous-batches). |
| A batch conflict | Resend a batch ID with a different body | [`409 batch_conflict`](/problems/batch_conflict) |
| An unverified book | Declare a complete book with the wrong `expectedReceivableCount` | [`409 book_completion_unverified`](/problems/book_completion_unverified) |
| Recovery stays held | Authorize recovery with a recovery instruction on a connection not yet activated for collection | `201` with `recoveryHeld: true`. See [Recovery entry](/concepts#recovery-entry). |
| A rate limit | Send two requests at once with one key | [`429 rate_limit_exceeded`](/problems/rate_limit_exceeded) with `Retry-After` |
| An unknown path or method | `GET /v1/nothing`, or `DELETE` on a Customer | `404 route_not_found` or `405 method_not_allowed` |

## Sandbox limits

The only sandbox-specific limit is the per-Client rate limit. The others apply in production too.

| Limit | Value |
| - | - |
| Requests per minute | 60 per key, and 60 per Client across all keys in sandbox |
| Requests in flight | 1 per key, and 1 per Client in sandbox |
| Request body | 256 KiB |
| Batch | 100 records, 2 queued or running batches per connection |
| Balance snapshot | 100 Receivables |
| Change feed page | 1 to 500, default 100; 30 days retained |
| Webhook delivery | 5 second timeout, 8 attempts, 30 days retained |

Sandbox sends messages only to recipients on PayPathIQ's sandbox allowlist. Payment integrations use test-mode credentials. See [Environments](/environments).

## What is not available yet

* Special IDs or amounts that trigger specific outcomes.
* A sandbox reset or delete operation.
* A test-event endpoint for webhooks.
* A way to expire a change-feed cursor early.
* A way to switch your own connection between `transaction` and `snapshot_only`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.