# Pagination, errors and idempotency

> How lists are paged, how errors look, how to retry a write safely and how money and dates are formatted.

Source: https://docs.bakasir.com/en/docs/developers/conventions

## Responses

A single object comes back as `data`:

```json
{ "data": { "id": "01K9ZT6Q2X3M8N4V7B5C1D0EFG", "name": "Es Kopi Susu" } }
```

A list comes back as `data` with paging fields:

```json
{ "data": [], "has_more": false, "next_cursor": null }
```

## Pagination

Lists use cursors. Pass `limit` (1 to 200, default 50) and, for the next page, the `next_cursor` of the previous one:

```bash
curl "https://api.bakasir.com/v1/sales?limit=100&cursor=MDFLOVpUNlEyWDNNOE40VjdCNUMxRDBFRkc" \
  -H "Authorization: Bearer bak_your_key_here"
```

- Results are ordered from oldest to newest. Add `order=desc` for newest first.
- Keep going while `has_more` is `true`. When it is `false`, `next_cursor` is `null`.
- A cursor is opaque. Do not build or edit one. A bad cursor returns `400` with the code `invalid_cursor`.
- New records created while you page appear at the end, so a full sync can simply continue until `has_more` is `false`.

## Errors

Every error has the same shape. `code` is stable and meant for your program, `error` is a human readable sentence.

```json
{ "error": "not found", "code": "not_found" }
```

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | The body or a query value is not valid |
| 400 | `invalid_cursor` | The cursor is not valid |
| 401 | `invalid_api_key`, `revoked_api_key` | See [Authentication](https://docs.bakasir.com/en/docs/developers/authentication.md) |
| 403 | `insufficient_scope`, `plugin_inactive`, `tenant_inactive` | See [Authentication](https://docs.bakasir.com/en/docs/developers/authentication.md) |
| 403 | `subscription_read_only`, `subscription_suspended` | The subscription is overdue, so writing or reading is blocked |
| 403 | `email_verification_required` | The owner has not verified the email address yet |
| 404 | `not_found` | The record does not exist or belongs to another business |
| 409 | `conflict` | The change clashes with existing data, for example a duplicate product code |
| 413 | `payload_too_large` | The body is larger than 1 MB |
| 422 | `unknown_store`, `unknown_product` | A referenced outlet or product does not exist |
| 422 | `idempotency_key_reused` | The `Idempotency-Key` was used with a different request |
| 429 | `rate_limited` | Too many requests |
| 5xx | `server_error`, `unavailable` | Something failed on our side. Retry with backoff |

## Idempotency

Networks fail. To retry a write without creating it twice, send an `Idempotency-Key` header with any unique value up to 200 characters on `POST`, `PATCH` and `DELETE` requests:

```bash
curl https://api.bakasir.com/v1/customers \
  -H "Authorization: Bearer bak_your_key_here" \
  -H "Idempotency-Key: import-2026-11-04-row-18" \
  -H "Content-Type: application/json" \
  -d '{"name": "Sari Dewi", "phone": "081200000000"}'
```

- The first response is stored for 24 hours. A retry with the same key and the same body returns it again, with the header `Idempotent-Replayed: true`, and nothing is created twice.
- The same key with a different request returns `422` `idempotency_key_reused`.
- A request that is still running returns `409` `idempotency_in_progress`. Wait a moment and retry.
- Server errors (5xx) are not stored, so you can retry them with the same key.

## Money, quantities and dates

- Amounts such as `price`, `cost` and `total` are integers in the smallest unit of the business currency. For rupiah, `22000` means Rp22.000. For a currency with cents, `1250` means 12.50.
- Quantities are numbers and may have decimals, for example `0.5` kilogram.
- Timestamps are RFC 3339 in UTC, for example `2026-11-04T08:30:00Z`. In filters such as `from` and `to` you can also send a date like `2026-11-04`, which is read in the business time zone.
- IDs are text. Treat them as opaque.
