---
url: https://docs.dataflair.ai/marketplace/ad-server-api/conventions.md
description: >-
  Rules that apply to every endpoint of the Ad Server API. Transport, dates,
  currency, idempotency, pagination, rate limits and the error model.
---

# Conventions

Rules that apply to every endpoint. Get these right and the integration works on the unhappy paths as well as the happy one.

## Transport and versioning

* **HTTPS only.** DataFlair rejects plain HTTP before it sends a request.
* **JSON in, JSON out.** Send `Content-Type: application/json` on requests with a body. Return `application/json`.
* **Version in the path.** Root your API at a versioned base, for example `https://ads.example.com/api/v1`. Ship breaking changes as `/v2`. Do not change the meaning of a field in place.

## Dates, timezone and flights

* All dates are `YYYY-MM-DD`. They are calendar dates, not timestamps, unless a field is explicitly a datetime.
* Dates are read in the **account timezone** you return from [`GET /health`](/marketplace/ad-server-api/operations/health). DataFlair sends flight `start_date` and `end_date` in that zone. A flight of `2026-08-01` to `2026-08-31` means the whole of August in your timezone, including both ends.
* Declare the timezone. GAM and Revive both drive line item flights from the network timezone. A silent mismatch is the classic "the campaign started a day early or late" bug.

## Currency

* Use ISO-4217 codes (`EUR`, `USD` and so on), declared once in `GET /health`.
* DataFlair checks a booking's currency against it before it pushes an order. It stops on a mismatch and does not guess. Its GAM integration follows the same discipline.
* You do not receive prices on the wire. Billing belongs to DataFlair.

## Units

* Impressions and clicks are integers.
* Impression goals and forecast numbers are impression counts (`unit_type: "IMPRESSIONS"`).

## Idempotency {#idempotency}

* Write calls (`POST /orders`) carry an `idempotency_key` in the body and an `Idempotency-Key` header with the same value.
* `idempotency_key` protects one call against being replayed. It is not a stable identifier for the life of the order. A repeat with the **same key and the same body** must return the **same result** (`200`) and create nothing new. A repeat with the **same key and a different body** is a `409`. It means something upstream retried incorrectly. Do not apply the new body.
* To update an order or line item you already created, for example to attach a creative once it clears approval, DataFlair sends a **new** `POST /orders`. It carries a **fresh** `idempotency_key` and the **same** `order.external_ref` and `line_items[].external_ref` as the original call. Match on `external_ref`, update only what changed, and return `200` with the existing `order_id` and `line_item_id` values. The full recovery contract is on [POST /orders](/marketplace/ad-server-api/operations/orders#idempotency-and-recovery).
* Read calls (`/health`, `/inventory`, `/forecast`, `/reports`) are idempotent by nature. DataFlair can repeat them freely.

## Pagination

List responses (`GET /inventory`, and any large `POST /reports`) use **cursor** pagination, not offset.

* **Request.** `limit` and `cursor` travel as query parameters on a `GET` (`?limit=<n>&cursor=<opaque>`). On a `POST` they travel as body fields next to the rest of the JSON payload, because that call already has a body. Use a sane default for `limit` (for example 50) and a cap (for example 200).
* **Response.** Include `next_cursor`. Leave it out, or return `null`, on the last page.
* Keyset paging, not `OFFSET`, keeps DataFlair from missing or double-reading rows when data changes in the middle of a page.

## Rate limiting

* If DataFlair goes over a budget, return `429 Too Many Requests` with a `Retry-After` header in seconds.
* DataFlair backs off and retries per `Retry-After`. It does not hammer your server. Publish your per-credential budget in your handoff notes so DataFlair can pace itself.

## Error model {#error-model}

Every non-2xx response returns a JSON body:

::: code-group

```json \[supplemental/forecast-unknown-slot/expected-response.json]
{
  "code": "inventory_not_found",
  "message": "No ad slot exists for inventory_id 'slot_x'.",
  "details": {
    "inventory_id": "slot_x"
  }
}

```

:::

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `code` | string | yes | A stable, machine-readable snake\_case slug. |
| `message` | string | yes | Human-readable text. DataFlair keeps it as the last error. Do not put secrets in it. |
| `details` | object | no | Optional structured context. |

## HTTP status usage {#http-status-usage}

| Status | When | DataFlair's reaction |
| --- | --- | --- |
| `200`, `201` | Success | Proceeds |
| `400` | Malformed request | Treats it as a bug, logs it and shows it |
| `401` | Missing, invalid or expired credential | Marks the connection as `error`. The operator re-checks the credential. |
| `403` | Authenticated, but missing a scope | Marks the connection as `error`. The operator widens the credential's scope. |
| `404` | Unknown `inventory_id`, `order_id` or `line_item_id` | Skips that item and reports it. It does not force the item onto a booking. |
| `409` | Idempotency conflict (same key, different payload) | Logs it. DataFlair does not retry it blindly. |
| `422` | Valid shape, invalid values (a bad date range, an incompatible size) | Shows your `message`, so the operator can fix the booking |
| `429` | Rate limited | Backs off per `Retry-After` and retries |
| `501` | A capability is not implemented (for example forecast for one slot) | Degrades gracefully. See [POST /forecast](/marketplace/ad-server-api/operations/forecast#if-you-cannot-forecast). |
| `5xx` | Server error | Retries with backoff. A persistent `5xx` shows as "your platform is unreachable". |

## Security expectations

* Hash credentials at rest. Do not log them in plaintext. DataFlair holds the keys it stores to the same rule.
* Do not echo the credential, or any secret, in a response body or an error.
* Do not put credentials or personal data in a URL or query string. DataFlair sends credentials only in the `Authorization` header.
* The credential you issue must be **revocable**, so either side can rotate it if it leaks, without downtime.
