# Sandbox and testing

> Test your integration end to end with robo drivers, no real SMS and directives for every failure path.

Source: https://docs.delivr.no/sandbox/

The sandbox is a separate Delivr environment to build and test against. It behaves like production, with three differences:

- **No real SMS.** Recipient messages are recorded, not sent. Use any valid Norwegian phone number, such as `+4791234567`, and open the order's `trackingUrl` to see what the recipient sees.
- **Robo drivers.** Test drivers deliver your on-demand orders in about two minutes.
- **Test data only.** Never send real customers' names, phones or addresses.

|                |                                                                                                       |
| -------------- | ----------------------------------------------------------------------------------------------------- |
| Base URL       | `https://driver-staging-api.delivr.no`                                                                |
| Tracking pages | `https://tracking-stage.delivr.no`                                                                    |
| Keys           | Sandbox keys (`dmk_…`), see [Get sandbox access](#get-sandbox-access). They never work in production. |

## Get sandbox access

Ask your Delivr contact for a sandbox merchant. Send:

- your company name and organization number, and the market (Norway),
- who should receive the key,
- the scope you need: `orders` to book deliveries, or `quotes` for a backend that only prices them (see [Scopes](/authentication/#scopes)),
- your webhook URL, if you want webhooks (public HTTPS),
- the [fallback policy](/guides/fallbacks/) you want to test, if not the default `recipient_choice`.

Delivr creates a sandbox merchant and sends you the API key and, with a webhook URL, its signing secret (`whsec_…`). Each is shown once, so store them in a secret manager.

### Pick a webhook URL that stays the same

Only Delivr can change your webhook URL, so use one that does not change:

- a tunnel with a fixed name, such as an ngrok static domain or a Cloudflare named tunnel, to reach your laptop,
- or a request inspector such as webhook.site, to look at events before you have a handler.

A free ngrok URL changes every time you restart it. Until your endpoint is set up, [test your handler locally](/webhooks/testing/).

## Getting set up

1. Add your stores with [`POST /v1/merchant/locations`](/guides/locations/). Robo drivers park at a new store within a minute.
2. Check your webhook endpoint with [`GET /v1/merchant/webhook`](/api/operations/getwebhook/) (`409 conflict` means none is set up yet).
3. Send a test event with [`POST /v1/merchant/webhooks/test`](/api/operations/sendwebhooktest/), then [`GET /v1/merchant/webhooks/deliveries`](/api/operations/listwebhookdeliveries/) to see what your server answered.

## Robo drivers

Robo drivers park outside sandbox stores, online. Because they are nearby, quotes for your store offer `on_demand`, and when an order's goods are released one of them accepts it.

A robo driver behaves like a real driver using the Delivr driver app:

| Step                                   | Takes                                 | Status and webhook                                    |
| -------------------------------------- | ------------------------------------- | ----------------------------------------------------- |
| Accepts the offer                      | A few seconds after the search starts | `driver_assigned`, `order.driver_assigned`            |
| Drives to the store                    | About 45 seconds                      |                                                       |
| Arrives and loads, with a pickup photo | 5 to 20 seconds                       | `picked_up`, `order.picked_up`                        |
| Drives to the recipient                | About 45 seconds                      |                                                       |
| Delivers, with a drop-off photo        |                                       | `delivered`, `order.delivered` with `proofOfDelivery` |

The recipient's tracking page shows the robo driver moving on the map, just as a shopper would see a real one.

### Where robo drivers park

The sandbox is shared by every integrator, and so are its robo drivers:

- Stores are grouped into cells of about 100 m. Stores of different merchants at the same address share a cell.
- Two robo drivers park in each of the **30 newest cells**, counted across all integrators. They return there after each delivery.
- Robo drivers only take Merchant API orders.

So two things can happen:

- **Both drivers of your cell are busy**, for example because another integrator uses the same address. A quote then has no `on_demand` option until one is back. Wait a few minutes, or use a store at another address below.
- **Your store has dropped out of the 30 newest cells** because others added stores since. Its quotes stop offering `on_demand`. Add a new location (a new `externalStoreId`) at another address, and robo drivers park there within a minute. Locations cannot be removed, so give test stores clear names.

### Test addresses

These Oslo addresses are in the national address register, so they work without coordinates. Each store address is in its own cell.

| Use as a store                | Use as a delivery address            |
| ----------------------------- | ------------------------------------ |
| Akersgata 45, 0158 Oslo       | Karl Johans gate 22, 0026 Oslo       |
| Storgata 1, 0155 Oslo         | Møllergata 20, 0179 Oslo             |
| Pilestredet 48, 0167 Oslo     | Dronning Eufemias gate 16, 0191 Oslo |
| Hegdehaugsveien 31, 0352 Oslo |                                      |
| Bogstadveien 50, 0366 Oslo    |                                      |
| Grønlandsleiret 25, 0190 Oslo |                                      |

An address that is not in the register answers `422 invalid_address`. Check the postal code: Karl Johans gate 22 is `0026`, not `0159`.

## Testing failure paths with directives

Real drivers read `pickup.note`. Robo drivers do too, and they follow **directives** written there. Put one anywhere in the note of the order you place:

```json {5}
{
  "quoteId": "…",
  "option": "on_demand",
  "externalRef": "test-no-driver-1",
  "pickup": { "note": "sandbox:no_driver" },
  "delivery": { "…": "…" }
}
```

| Directive                | What the robo driver does                                                                   | What you can test                                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| _(none)_                 | Accepts and delivers.                                                                       | The happy path and every status webhook.                                                                             |
| `sandbox:no_driver`      | Every robo driver declines, and the search ends within seconds instead of after 15 minutes. | `failed` with reason `no_driver`, and your [fallback policy](/guides/fallbacks/).                                    |
| `sandbox:hand_back`      | The first driver accepts, sets off and gives the job back. Another driver then delivers it. | A driver handing a job back. For [assisted checkout](/guides/assisted-checkout/), `sale.driver` shows `reassigning`. |
| `sandbox:stop_en_route`  | Accepts and stays on the way to the store.                                                  | Cancelling at the `driver_en_route` fee.                                                                             |
| `sandbox:stop_at_pickup` | Accepts, arrives at the store and waits there.                                              | Cancelling at the `driver_arrived` fee, and goods that are not ready when the driver arrives.                        |

> **Start the search:** A robo driver reads the directive when it gets the offer, that is when the driver search starts. With `readySignal:
> assumed` that is `prepMinutes` after you place the order. To test straight away, send [`POST /v1/merchant/orders/{id}
> /ready`](/api/operations/markready/) after placing, or use a store with `prepMinutes: 0`.

Directives are not case-sensitive and can sit next to normal text, like `"Ask for Kari. sandbox:stop_at_pickup"`. A store's `pickupInstructions` work too, which is handy for a store you only use to test one path. In production, directives are ordinary text that drivers ignore.

A held driver (`stop_*`) gives the job back after an hour if you do nothing. The order then searches again, and the next robo driver reads the same directive and holds it too. Cancel the order to end the test.

## Timings

These are the defaults. A market's price list or your agreement can set others.

| What                                    | Default                                          |
| --------------------------------------- | ------------------------------------------------ |
| Quote offered (`expiresAt`)             | 5 minutes                                        |
| Quote grace when you place after expiry | 10 minutes                                       |
| On-demand driver search                 | 15 minutes, then `failed` with `no_driver`       |
| Ready grace after `readyBy`             | 15 minutes, then `failed` with `goods_not_ready` |
| Checkout budget (assisted checkout)     | 10 minutes from the driver's accept              |
| Recipient's window choice (fallback)    | 30 minutes (`choiceDeadline`)                    |

## More things to test

| Scenario              | How                                                                                                                                                                                                                                                                        |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Goods not ready       | Set your store's `readySignal` to `required`, place an order with a `readyBy` 5 minutes ahead, and send no `ready` or `processing`. You get `order.ready_overdue` at `readyBy`, then the order fails with `goods_not_ready` after the ready grace (15 minutes by default). |
| Packing time          | Send `POST /orders/{id}/processing {readyAt}` a few minutes ahead. The driver search starts so that the driver arrives at about `readyAt`.                                                                                                                                 |
| Price changes         | Place an order on a quote more than 15 minutes old (validity plus grace). Delivr quotes again: you get the order at no more than the quoted price, or `409 price_changed`.                                                                                                 |
| Free cancellation     | Place an order and cancel it before you send `ready`, while it is still `awaiting_goods`, with `maxFeeMinor: 0`.                                                                                                                                                           |
| Cancellation fees     | Place with `sandbox:stop_en_route` or `sandbox:stop_at_pickup`, send `ready`, wait for `driver_assigned`, then check `GET /orders/{id}/cancellation` and cancel.                                                                                                           |
| Fallback as recipient | Place with `sandbox:no_driver` and send `ready`. Under `recipient_choice`, open the failed order's `trackingUrl` and choose a window, as the recipient would.                                                                                                              |
| Webhook retries       | Answer `500` from your endpoint, then look at `GET /v1/merchant/webhooks/deliveries` for the retry schedule. Replay an event with `POST /v1/merchant/webhooks/events/{id}/replay`.                                                                                         |
| Assisted checkout     | Place with `sale: "pending"`, poll until `sale.driver` is `assigned`, then complete with `POST /orders/{id}/sale`.                                                                                                                                                         |

## Window orders

Window orders can be quoted, placed, rescheduled and cancelled in the sandbox. Robo drivers do not run window routes, so a window order usually stays `scheduled`. If you need to see `picked_up` and `delivered` on a window order, ask your Delivr contact to run one for you.

## Limits and data

- The same [rate limits](/conventions/rate-limits/) apply as in production.
- The sandbox is shared by every integrator, and Delivr may reset its data. Do not depend on old orders being there.
- Prices come from a sandbox agreement and are not your production prices.