# Recipe: export yesterday's sales

Pull every sale completed yesterday into your own system with a date filter and a cursor loop.

Source: https://getolet.com/resources/developers/recipe-export-sales/
Updated: 2026-10-15

Call `GET /sales` with `completed_from` and `completed_to` set to yesterday's window, then follow `next_cursor` until `has_more` is `false`.

## How do I filter to one day

`completed_from` and `completed_to` are ISO-8601 datetimes filtered against `completed_at`. They must be in UTC and end in `Z` (for example `2026-09-23T00:00:00Z`); an offset such as `-04:00` is refused. Both ends are inclusive, so end the window one second early to keep a sale at exactly midnight out of two days. Run this once a night for the previous calendar day in your store's own timezone, converted to UTC.

```bash
curl "https://api.oletapp.com/your-store/v1/sales?completed_from=2026-09-23T00:00:00Z&completed_to=2026-09-23T23:59:59Z&limit=100" \
  -H "Authorization: Bearer olet_live_YOUR_API_KEY"
```

```json
{
  "data": [
    { "id": "...", "status": "completed", "completed_at": "2026-09-23T14:02:11Z", "outlet_id": "...", "version": "..." }
  ],
  "next_cursor": "...",
  "has_more": true
}
```

## How do I get every page

Keep calling with the last `next_cursor` you received until `has_more` is `false`.

```js
const base = 'https://api.oletapp.com/your-store/v1/sales'
let cursor
let sales = []
do {
  const url = new URL(base)
  url.searchParams.set('completed_from', '2026-09-23T00:00:00Z')
  url.searchParams.set('completed_to', '2026-09-23T23:59:59Z')
  url.searchParams.set('limit', '100')
  if (cursor) url.searchParams.set('cursor', cursor)
  const page = await (await fetch(url, { headers: { Authorization: 'Bearer olet_live_YOUR_API_KEY' } })).json()
  sales = sales.concat(page.data)
  cursor = page.next_cursor
  if (!page.has_more) break
} while (cursor)
```

## What is in each row

`GET /sales` returns the same full sale document as `GET /sales/{id}`: line items, discounts, adjustments, taxes and payments. There is no slimmer summary shape, so you rarely need the single-sale read in an export loop. Totals such as `total`, `tax_total` and `paid_total` are integer cents; see [money and quantities](/resources/developers/money-and-quantities/).

You can also narrow the list with `status` (`completed`, `partially_refunded`, `refunded` or `voided`), `outlet_id`, `customer_id` and `parent_sale_id`.

## Does this ever include a refund

Yes. A refund is a sale with `parent_sale_id` set. Add `parent_sale_id` as a query filter if you want only the refunds of one original sale, or leave it out to get both original sales and their refunds in the same window.

## What is deliberately excluded

`status=open` is refused with `400 VALIDATION_FAILED`: a parked sale is register state mid-transaction, not something to export. The list already excludes it by default, so this only matters if you pass `status` explicitly. `include_deleted` is also refused: sales are never deleted, so there is nothing to include.

A sale that is refunded later moves to a new `version`, so a later poll that uses the cursor will return it again with its new status. If you export by date window instead, re-export recent days to catch late refunds.

## Where to go next

See [pagination and polling](/resources/developers/pagination-and-polling/) for what `next_cursor` and `has_more` mean in general, or the full [API reference](/resources/developers/reference/) for every field on a sale.
