# Filtering and sorting

> The POST /filter operations read records that match filters, in the order you choose. The body format, the operators each field type allows, and the nuances of dates and sorting.

Web page: https://developer.yoprint.com/concepts/filtering-and-sorting/

To find records that match conditions, use a resource's **filter** operation. Filters use `POST` because they carry a body, but they only read.

> **Caution: Sales orders and quotes are v2-only**
>
> The legacy v1 filters for sales orders and quotes no longer work. Always use the v2 filter operations:
>
> `POST /v2/api/store/{team_slug}/v2/sales_order/filter` [List sales orders](https://developer.yoprint.com/reference/sales-orders/v2-sales-order-filter.md)
>
> `POST /v2/api/store/{team_slug}/v2/quote/filter` [List quotes](https://developer.yoprint.com/reference/quotes/v2-quote-filter.md)

```bash
curl https://secure.yoprint.com/v2/api/store/your-team/v2/sales_order/filter \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "X-Yoprint-Decimal-Format: exact-v1" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      { "key": "is_rush", "operator": "eq", "value": true },
      { "key": "due_date", "operator": "lte", "value": 7 }
    ],
    "sort": [{ "key": "due_date", "direction": "asc" }]
  }'
```

This returns rush sales orders due before the 7th day from today starts in UTC (overdue ones included), soonest first, one [page](https://developer.yoprint.com/concepts/pagination.md) at a time. Date values count UTC days, and v1 filters count them differently. See [Dates](#dates-are-days-from-today-and-v1-and-v2-count-differently).

## The body

| Field     | Meaning                                                                                                   |
| --------- | --------------------------------------------------------------------------------------------------------- |
| `filters` | A list of conditions. Each has a `key`, an `operator` and a `value`. A record must match every condition. |
| `sort`    | A list of sort objects, each with a `key` and `direction` (`asc` or `desc`).                              |
| `page`    | The page number to return. See [Pagination](https://developer.yoprint.com/concepts/pagination.md).        |

### Where to find filter and sort keys

Each resource allows filtering and sorting only on specific supported fields. To find the exact keys and allowed operators for a resource:

- Open that resource's filter operation in the API reference, such as **List sales orders**, **Search customers with filters** or **Filter jobs**.
- The operation's reference page lists every accepted filter `key`, its value type, its supported operators, and every accepted `sort` key.
- Sending an unlisted or misspelled key, or using an operator not supported by that field's type, is refused with HTTP `422`.

## Operators by field type

The operators a condition can use depend on the type of its key:

| Field type                                                             | `value`                                       | Operators            |
| ---------------------------------------------------------------------- | --------------------------------------------- | -------------------- |
| Boolean, such as `is_rush`                                             | `true` or `false`                             | `eq`, `neq`          |
| Date, such as `due_date`                                               | A whole number of days from today, not a date | `eq`, `gte`, `lte`   |
| Number or amount                                                       | A number                                      | `eq`, `gte`, `lte`   |
| Reference to other records, such as statuses, tags, customers or users | A list of hash ids                            | `in`, `nin` (not in) |
| Single option, such as `payment_status` (`paid` or `unpaid`)           | A string                                      | `eq`                 |
| Multiple option                                                        | A list of strings                             | `in`, `nin`          |

A condition with a missing or `null` value, an unknown key or an operator its type doesn't allow is refused with `422`.

## Dates are days from today, and v1 and v2 count differently

A date condition's `value` is a whole number of days from today: `0` is today, `7` is a week from today and `-30` is 30 days ago. A date such as `"2026-11-12"` isn't accepted: it's refused with `422`. Days are **UTC days**, not your shop's days. The two filter versions also count from different points, and v1 counts `lte` **backwards**:

| Filters                                                                         | Count from                      | `lte N` matches                                                 |
| ------------------------------------------------------------------------------- | ------------------------------- | --------------------------------------------------------------- |
| **v2**: sales orders, quotes and invoices                                       | 00:00 UTC at the start of day N | Everything before day N starts. Day N itself is left out.       |
| **v1**: customers, jobs, tasks, shipments, packages, approvals and integrations | Now                             | On or before now **minus** N days                               |
| **The production control calendar** (a v1 path that counts forward, like v2)    | Today's UTC date                | On or before the date N days from today, counting the whole day |

**Diagram:** The same condition finds next week's work in v2, and only work from over a week ago in v1.

- **v2 · lte 7**: Everything before the 7th day from today starts, overdue included
- **v1 · lte 7**: Only dates 7 or more days ago
- **v1 · lte -7**: Everything up to 7 days from now: in v1, a negative number reaches forward

### v2 dates, exactly

YoPrint turns `N` into a date, today's UTC date plus `N` days, and compares the record's date and time with 00:00 UTC on that day:

| Operator | Matches                                                                                                              |
| -------- | -------------------------------------------------------------------------------------------------------------------- |
| `gte N`  | From 00:00 UTC on day N onwards.                                                                                     |
| `lte N`  | Up to 00:00 UTC on day N. Day N itself is left out: send `N + 1` to include it.                                      |
| `eq N`   | Only a date stamped exactly 00:00:00 UTC on day N. It matches almost nothing, so use a `gte` and `lte` pair instead. |

Dates the web app sets are stored as the end of the shop's day, in UTC. A due date of 12 November in New York is stored as `2026-11-13T04:59:59Z`, on the next UTC day. So a UTC day and your shop's day don't line up, and a filter can't pick your shop's days exactly. Ask for a slightly wider range, then keep the right rows in your code:

- **Your shop's days A to B** (in days from today): ask `gte A-1` and `lte B+2`, then keep the rows whose date falls on those days in your shop's time zone. The extra day at the start covers time zones ahead of UTC. The two extra days at the end cover time zones behind UTC and the left-out day N.
- **Overdue** (the date is before now): ask `lte 1`, then keep the rows whose date is before now.

### Worked example: due today and tomorrow in New York

Today and tomorrow are days 0 and 1. Ask for `gte -1` and `lte 3`, then keep the orders whose `due_date`, read in New York time, is today or tomorrow. With the `yoprint` helper from [Calling the API](https://developer.yoprint.com/concepts/calling-the-api.md):

```javascript
const zone = 'America/New_York';
// A date as YYYY-MM-DD, on the shop's calendar.
const shopDay = (date) => new Intl.DateTimeFormat('en-CA', { timeZone: zone }).format(date);

const today = shopDay(new Date());
const [y, m, d] = today.split('-').map(Number);
const tomorrow = new Date(Date.UTC(y, m - 1, d + 1)).toISOString().slice(0, 10);

const page = await yoprint('POST', '/v2/api/store/{team_slug}/v2/sales_order/filter', {
  filters: [
    { key: 'due_date', operator: 'gte', value: -1 },
    { key: 'due_date', operator: 'lte', value: 3 },
  ],
  sort: [{ key: 'due_date', direction: 'asc' }],
});

const dueTodayOrTomorrow = page.data.filter(
  (order) => order.due_date && [today, tomorrow].includes(shopDay(new Date(order.due_date))),
);
```

Read every page of the answer the same way. See [Pagination](https://developer.yoprint.com/concepts/pagination.md).

### Worked out for both versions

| You want                                    | v2 filters                                               | v1 filters                                    |
| ------------------------------------------- | -------------------------------------------------------- | --------------------------------------------- |
| Due from today through the 7th day from now | `gte -1` and `lte 9`, then keep your shop's days in code | `gte 0` and `lte -7` (now to 7 days from now) |
| Overdue                                     | `lte 1`, then keep the dates before now                  | `lte 0` (before now)                          |
| Created in the last 30 days                 | `gte -30` (from 00:00 UTC 30 days ago)                   | `gte -30` (from now minus 30 days)            |

**v1 date operators, exactly**

| Operator | v1: matches a date…                              |
| -------- | ------------------------------------------------ |
| `gte N`  | on or after now + N days                         |
| `lte N`  | on or before now **−** N days (counts backwards) |
| `eq N`   | anywhere in UTC day today + N                    |

**The invoice filter's order dates count whole days.** On the v2 invoice filter, the keys that read the invoice's order (`order_issue_date`, `full_amount_due`, `deposit_due`, `order_cust_due_date` and `order_ship_due_date`) compare whole UTC days. `lte N` includes all of day N, and `eq 0` matches anywhere in today's UTC day. The invoice's own `created_at` follows the v2 rule above. Asking for a wider range and narrowing it in your code works here too.

## Statuses, tags and other references

References take a list of hash ids, even for one value:

```json
{ "key": "status_id", "operator": "in", "value": ["BkzhmfgkK2HFTs", "ZXhN3KtjHgQZFz"] }
```

Get status ids from the status lists, such as:

`GET /v1/api/store/{team_slug}/setting/status/sales_order` [List sales order statuses](https://developer.yoprint.com/reference/statuses/v1-status-list-sales-order.md)

## Sorting

The `sort` array controls the order of returned records. Each sort item is an object with a field `key` and a `direction`:

```json
{
  "sort": [
    { "key": "created_at", "direction": "desc" }
  ]
}
```

- `direction`: `"asc"` for ascending (earliest date first, lowest number first, A to Z) or `"desc"` for descending (latest date first, highest number first, Z to A).
- **v2 filters (sales orders, quotes, invoices):** apply only the **first** entry in `sort`. Any subsequent entries in the array are ignored.
- **v1 filters (jobs, customers, shipments and others):** apply every entry in order, sorting by the first key and breaking ties with the second.
- **Default order:** on a v2 filter, an omitted or empty `sort` uses the resource's default order (typically `created_at` descending). v1 filters have no default order: send `sort` whenever the order matters.
- **Pagination stability:** sort by a field that doesn't change, such as `created_at`. See [Pagination](https://developer.yoprint.com/concepts/pagination.md).
- **Allowed keys:** You can only sort by keys listed on that resource's filter reference page. Unrecognized sort keys are refused with `422`.

## Comparing v1 and v2 filters

| Feature              | v1 filters                                                           | v2 filters                                                           |
| -------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Applicable resources | Customers, jobs, tasks, shipments, packages, approvals, integrations | Sales orders, quotes, invoices                                       |
| Empty `filters`      | Refused (`422`). Send at least one condition.                        | Allowed: returns every record, a page at a time.                     |
| Sort entries used    | All entries in order (multi-field sort)                              | The first entry only (single-field sort)                             |
| `lte` date operator  | Counts **backwards** into the past (`now - N`)                       | Counts **forwards** into the future (start of day + N)               |
| Count only           | No                                                                   | `"count_only": true` returns the `pagination` block without records. |

## Search instead of filter

To find an order from a few words, such as its number, its name or the customer's name, use search:

`GET /v2/api/store/{team_slug}/search/orders` [Search sales orders and quotes](https://developer.yoprint.com/reference/search/v2-search-orders-get.md)

It takes `q`, and optionally `customer_id` and `quote` (`1` for quotes only, `0` for sales orders only, or leave it out for both). `true` or `false` for `quote` is refused with `422`. It returns up to 30 matches.

## Choose what comes back

Many reads accept `with`, a list of related records to include, and `only`, a list of fields to return. Use them where an operation's reference page documents them. The v2 filters accept only some related records.
