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

# API conventions

> List responses, identifiers, timestamps, money, and filtering — the rules every endpoint follows

Conventions that hold across the whole API. Individual endpoints document what
is specific to them; everything here is assumed.

## Requests

|                     |                                                                                                                     |
| ------------------- | ------------------------------------------------------------------------------------------------------------------- |
| **Base URL**        | `https://api.offergrid.io`                                                                                          |
| **Protocol**        | HTTPS only                                                                                                          |
| **Auth header**     | `x-api-key: YOUR_TEAM_API_KEY` on every authenticated request — see [Authentication](/docs/api-reference/authentication) |
| **Request bodies**  | JSON. Send `Content-Type: application/json`.                                                                        |
| **Partial updates** | `PATCH` with only the fields you want changed. Omitted fields are left alone.                                       |

<Warning>
  **Unknown fields are rejected, not ignored.** Sending a property the endpoint
  does not define returns `400` with `"property <name> should not exist"`. This
  turns a typo into an immediate error instead of a silently dropped value — but
  it also means you cannot round-trip a response object straight back into a
  `PATCH`. Send only the fields you intend to change.
</Warning>

## List responses

<Note>
  **List endpoints are not paginated.** They return a complete JSON array of
  every matching record your team can see. There are no `page`, `limit`,
  `offset`, or `cursor` parameters, and no pagination envelope.
</Note>

```json theme={null}
[
  { "id": "1a2b3c4d-...", "name": "High-Speed Internet 1000 Mbps" },
  { "id": "5e6f7a8b-...", "name": "Fiber 500" }
]
```

This is fine at current catalog and order volumes and keeps clients simple. It
does mean a list response grows with your data, so:

* **Filter server-side where you can.** `GET /reseller/catalog` accepts
  `category`, `minPrice`, `maxPrice`, `zipCode`, and `search`; narrowing there
  is far cheaper than fetching everything and filtering locally.
* **Do not assume a bounded response size** in your client — no fixed buffers,
  no hard-coded array-length expectations.
* **Prefer the detail endpoint** when you already know the id.
  `GET /reseller/orders/{id}` beats scanning `GET /reseller/orders`.

<Tip>
  Pagination will be added additively — an opt-in query parameter with the
  unpaginated array as the default response — so it does not break existing
  clients. It will be announced in the [changelog](/docs/api-reference/changelog)
  before it ships. If unbounded lists are already a problem for you, tell us at
  [support@offergrid.io](mailto:support@offergrid.io).
</Tip>

## Filtering

Filters are query parameters, and they combine with AND. An unknown query
parameter is rejected with `400`, the same as an unknown body field.

```bash theme={null}
curl -G https://api.offergrid.io/reseller/catalog \
  -H "x-api-key: YOUR_TEAM_API_KEY" \
  --data-urlencode "category=internet" \
  --data-urlencode "zipCode=94102" \
  --data-urlencode "maxPrice=100" \
  --data-urlencode "search=fiber"
```

## Identifiers

Every resource id is a **UUID v4** string.

```
1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
```

Treat them as opaque: do not parse them, infer type or ordering from them, or
assume any length other than 36 characters. Ids are stable for the life of the
resource.

The one exception is a shareable link's `slug`, which appears in public URLs and
is a short human-shareable token rather than a UUID.

## Timestamps

All timestamps are **ISO 8601 in UTC**, with milliseconds and a trailing `Z`.

```json theme={null}
{ "createdAt": "2026-08-28T12:00:00.000Z" }
```

Send them in the same format. Date-only fields — a move-in date, an
installation date — are calendar dates with no time component and no timezone;
do not convert them through a local timezone or they can shift by a day.

## Money

Monetary values are returned as **strings**, not numbers.

```json theme={null}
{
  "monthlyPrice": "79.99",
  "promoMonthlyPrice": "49.99",
  "totalMonthly": "129.98"
}
```

<Warning>
  Parse money with a decimal library, not `parseFloat`. These are exact decimal
  values; binary floating point cannot represent them exactly, and rounding
  drift on prices and totals shows up in customer-visible numbers. Strings are
  used precisely so no precision is lost in transport.
</Warning>

All amounts are in **USD**, monthly, and before taxes and fees unless the offer
states otherwise. Prices on an order are snapshotted at order time — an offer
whose price changes later does not retroactively change existing orders.

## Nulls and optional fields

A field that does not apply is `null` rather than absent, so response shapes
stay stable. Two exceptions are genuinely conditional and absent when they do
not apply:

* `electricity` — present only on electricity offers
* `internet` — present only on internet offers

Every offer carries a top-level `serviceType` discriminator (`"electricity"`,
`"internet"`, …), so branch on that rather than probing for the grouped object.

<Note>
  Responses are **additive over time**. New fields can appear in any response
  without notice, so parse permissively and ignore what you do not recognize —
  a strict parser that rejects unknown response fields will break on a routine
  release. See [Versioning](/docs/api-reference/versioning).
</Note>

## Idempotency

There is no `Idempotency-Key` header today. `POST /reseller/orders` is not
idempotent: two identical requests create two orders.

If a write times out without a response, **reconcile before retrying** —
`GET /reseller/orders` will tell you whether the first attempt landed. See
[Handling errors](/docs/api-reference/errors#handling-errors).

Reads, `PATCH`, and `DELETE` are naturally idempotent and safe to retry.

## Next steps

<CardGroup cols={2}>
  <Card title="Errors" icon="triangle-exclamation" href="/docs/api-reference/errors">
    Every status code and a retry-safe client
  </Card>

  <Card title="Rate limits" icon="gauge-high" href="/docs/api-reference/rate-limits">
    What is limited, and what is not
  </Card>

  <Card title="Versioning" icon="code-branch" href="/docs/api-reference/versioning">
    What we promise not to break
  </Card>

  <Card title="Authentication" icon="key" href="/docs/api-reference/authentication">
    Keys, headers, roles, and rotation
  </Card>
</CardGroup>
