Skip to content
POST/orders We call you

DataFlair calls YOUR server. You implement this endpoint. You do not call DataFlair.

RequiredBuild this. It is part of the contract.

POST /orders: Create draft order ​

When you approve an advertiser's reservation in DataFlair, DataFlair pushes it into your ad server as an order with one draft line item for each booked inventory line. An inventory line is an ad slot, a geo and a month. A booking that covers two months, or two countries priced separately, arrives as several line items on the same slot. Your ad-ops team then reviews the draft and takes it live.

This is the same operation as GAM's "create a DRAFT order and line items" and Revive's "create an inactive campaign and banners".

DataFlair triggers this call when a reservation is approved. It can call it again for the same campaign. See Idempotency and recovery.

Draft only ​

DRAFT ONLY

DataFlair does not call activate, approve, go-live, unpause or publish. None of them is in the adapter, by design. Everything this endpoint creates must be a draft that does not serve.

  • Create the order and its line items in your platform's draft, paused or inactive state.
  • Do not activate, approve, unpause or start serving anything as a side effect of this call.
  • Return the objects in that non-serving state, and echo status: "DRAFT".

Going live is a human action in your console. DataFlair applies the same rule to Google Ad Manager, where it does not call performOrderAction. It applies it to Revive, where it creates banners as INACTIVE and does not link them to a zone. If your POST /orders made inventory serve at once, it would break the guarantee DataFlair gives every publisher.

What we send ​

http
POST /orders HTTP/1.1
Host: ads.example.com
Authorization: Bearer sk_live_9f2c…
Content-Type: application/json
Idempotency-Key: DF-CMP-VDWZQ4IT
bash
curl -X POST "$BASE_URL/orders" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: DF-CMP-VDWZQ4IT" \
  -d @request.json

The body, saved as request.json:

json
{
  "idempotency_key": "DF-CMP-VDWZQ4IT",
  "advertiser": {
    "name": "Acme Corp",
    "external_ref": "brand_5501"
  },
  "order": {
    "name": "Summer Launch (DF-CMP-VDWZQ4IT)",
    "external_ref": "CMP-VDWZQ4IT"
  },
  "line_items": [
    {
      "external_ref": "DF-CMP-VDWZQ4IT-329",
      "inventory_id": "slot_728x90_home",
      "flight": {
        "start_date": "2026-08-01",
        "end_date": "2026-08-31"
      },
      "goal_impressions": 100000,
      "targeting": {
        "geo": [
          "DE",
          "AT"
        ]
      },
      "sizes": [
        "728x90"
      ],
      "creatives": [
        {
          "type": "third_party_tag",
          "width": 728,
          "height": 90,
          "tag": "<iframe src='https://t.dataflair.ai/ad/ABC123?df_source=custom&df_ad_server=custom_platform' width='728' height='90' frameborder='0' scrolling='no' referrerpolicy='no-referrer-when-downgrade' sandbox='allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation allow-same-origin' style='border:0;display:block' title='Advertisement'></iframe>"
        }
      ]
    }
  ]
}

The JSON on this page is read from the fixture files, so it cannot drift from the test suite.

Fields ​

FieldTypeRequiredMeaning
idempotency_keystringyesUnique to this call, not to the campaign. Reuse it only to retry the exact same request. A retry with the same key and a different body is rejected (see below). DataFlair also sends it as the Idempotency-Key header.
advertiser.namestringyesThe buyer. Resolve or create an advertiser by this name (see below).
advertiser.external_refstringnoDataFlair's opaque id for the advertiser, if you want to store it.
order.namestringyesA human label, for example Summer Launch (DF-CMP-VDWZQ4IT). It is deterministic, so a lost response can be recovered by name.
order.external_refstringyesDataFlair's campaign reference, stable for the life of the campaign. Match on this, not on idempotency_key, to recognize an update to an order you already created (see below).
line_items[].external_refstringyesDataFlair's stable id for this booked line. It is unique even when a booking splits one slot into several lines by geo or month, for example DF-{campaign}-{line} and not DF-{campaign}-{slot}. Two lines can share a slot, and they cannot share an external_ref. Return it in the response so DataFlair can match your line_item_id to it.
line_items[].inventory_idstringyesThe slot to book. See Inventory identity.
line_items[].flightobjectyesInclusive start_date and end_date, YYYY-MM-DD, in your account timezone.
line_items[].goal_impressionsintegeryesThe impression goal the advertiser bought for this line. Feed it into your delivery and pacing engine.
line_items[].targeting.geoarray of stringsnoISO-3166-1 alpha-2 countries. Absent or empty means worldwide.
line_items[].sizesarray of stringsyesCreative size or sizes for the line. They must fit the slot.
line_items[].creativesarrayrecommendedThe creative or creatives to serve: a ready-made tag from DataFlair, stored and served as sent. See How creatives work.

How creatives work ​

You do not receive an image to host. DataFlair supplies a ready-to-serve tag that points back to DataFlair, and your platform stores and serves that tag as sent. The creative asset lives on DataFlair's CDN. An advertiser can swap the approved creative mid-flight, and nothing on your side changes.

DataFlair sends the creative in one of two shapes, depending on what your platform's creative model supports.

A. Third-party or HTML tag (primary). An <iframe> that points at DataFlair's ad frame. The request above carries this shape.

html
<iframe src="https://t.dataflair.ai/ad/ABC123?df_source=custom&df_ad_server=custom_platform"
  width="728" height="90" frameborder="0" scrolling="no"
  referrerpolicy="no-referrer-when-downgrade"
  sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation allow-same-origin"
  style="border:0;display:block" title="Advertisement"></iframe>

Store it as a third-party or HTML creative and serve it into the slot. GAM stores it as a ThirdPartyCreative. Revive stores it as banner HTML. When it renders, the frame loads the current approved creative from DataFlair's CDN. It also fires its click (/go/{code}) and impression (/imp/{code}) tracking from inside the frame. You do not build the iframe. DataFlair builds it, and you store it as it arrives.

Treat the tag as an opaque string

The /ad, /go and /imp paths are DataFlair's live production endpoints. The link code (ABC123), the asset host and the query parameters (df_source=…, DataFlair's source attribution tag, minted per platform when the adapter is built) are illustrative here. The live tag arrives ready-made for each line item. Store it and serve it exactly as received.

B. Image fields (fallback). If your platform only accepts a hosted image with a click-through and cannot serve an HTML tag, DataFlair sends the pieces as structured fields.

json
{
  "type": "image",
  "width": 728, "height": 90,
  "image_url": "https://cdn.dataflair.ai/c/ABC123.png",
  "click_url": "https://t.dataflair.ai/go/ABC123?df_source=custom&df_ad_server=custom_platform",
  "impression_pixel_url": "https://t.dataflair.ai/imp/ABC123?df_source=custom&df_ad_server=custom_platform"
}

Wire the creative's click-through to click_url, and fire impression_pixel_url when it renders.

Rules for both shapes:

  • Keep the /go/{code} click link exactly as sent. Do not rewrite it or strip it. Every DataFlair campaign must serve through it, because that is how clicks are attributed and reconciled. A line that bypasses it cannot be reconciled.
  • You still count impressions natively. The impression count your ad server keeps is what you return in reports. The in-frame pixel is DataFlair's independent cross-check. It does not replace your count.
  • No approved creative yet? Create the line item empty, as a draft. When the creative is approved, expect a follow-up POST /orders that carries it. It has a new idempotency_key and the same order.external_ref and line_items[].external_ref. Match on external_ref and update the existing line in place (see below).
  • Optional cache-buster. If your platform can fill a cache-buster macro inside a third-party tag, as GAM does with %%CACHEBUSTER%%, tell DataFlair, and it will append one for tighter impression reconciliation. If your platform cannot, DataFlair behaves as it does for Revive, with no macro.

Where each party sees the creative: your ad-ops see the tag, with a preview, on the line item in your console. The advertiser manages the actual asset inside DataFlair. The site visitor sees the rendered ad once a person takes the line live.

What you return ​

Status 201 when you create the order. A repeat or an update returns 200 with the same ids.

json
{
  "order_id": "ord_55021",
  "status": "DRAFT",
  "line_items": [
    {
      "external_ref": "DF-CMP-VDWZQ4IT-329",
      "line_item_id": "li_88012",
      "status": "DRAFT"
    }
  ]
}
FieldTypeRequiredMeaning
order_idstringyesYour stable id for the order container. DataFlair stores it.
statusstringyesEcho "DRAFT".
line_items[].external_refstringyesThe same external_ref DataFlair sent, so it can match the pair.
line_items[].line_item_idstringyesYour stable id for the line you created. DataFlair stores it and reconciles delivery against it in reports. It must not change for this booked line.
line_items[].statusstringyes"DRAFT".

Advertiser resolution ​

Resolve or create the advertiser from advertiser.name. If an advertiser with that name exists on your platform, reuse it. Otherwise create it. DataFlair does the same on GAM (getOrCreateAdvertiser) and on Revive (find or add by a deterministic name), so a repeat booking from the same brand does not create duplicate advertiser records. You do not need to store DataFlair's external_ref unless it helps you.

Idempotency and recovery ​

DataFlair can call POST /orders more than once for the same campaign. It happens on an automatic retry, when a publisher clicks "re-push", and when a later creative approval re-traffics. There are two cases. They use different keys.

  • Literal retry: the same idempotency_key. DataFlair repeats the exact same call, for example because the response to the first attempt was lost. Return the same result and create nothing new. If the same key arrives with a different body, something upstream is broken. Reject it with 409. Do not guess which body is the right one. See Conventions.
  • Update: a new idempotency_key and the same external_ref. A later creative approval, or any other change to an order or line you already created, arrives as a new call. It has a fresh idempotency_key and the same order.external_ref and line_items[].external_ref values you were sent at first. Match on external_ref. Update only the fields that changed and leave everything else alone. Return the same order_id and line_item_id values with 200.
  • In both cases, create only the lines that are genuinely missing. Do not duplicate an advertiser, an order or a line.

DataFlair records external_ref as a pending mapping before it calls you. It fills in your line_item_id when the response arrives. If the response is lost, it recovers the mapping by looking up the same external_ref on the retry. So re-pushing is safe as long as your side keys retry safety on idempotency_key and update matching on external_ref. The GAM and Revive integrations use the same deterministic naming recovery.

Currency ​

DataFlair checks that the booking currency matches your account currency (from GET /health) before it pushes. You do not receive a price on the line, because billing stays entirely in DataFlair. The flight, the goal, the targeting and the creative are everything your ad server needs to serve once a person activates the line.

Errors ​

StatusWhat caused itWhat DataFlair doesWhat to do
400Malformed request.Treats it as a bug, logs it and shows it.Check the body against the fields above.
401Missing, invalid or expired key.Marks the connection as error.Check the key.
403The key lacks a required scope.Marks the connection as error.Give the key create-draft scope.
404Unknown inventory_id.Skips the item and reports it.Return 404 only for a slot that does not exist.
409The same idempotency_key arrived with a different body.Logs it. DataFlair does not retry it blindly.Do not apply the new body. Reject the call.
422Valid shape, invalid values, such as a bad date range or an incompatible size.Shows your message, so the operator can fix the booking.Return a clear message.
429Rate limit exceeded.Backs off and retries per Retry-After.Send Retry-After in seconds.
5xxServer error.Retries with backoff.Check your server's logs.

Every error body has the shape described in Conventions.

Timeouts and retries ​

DataFlair does not call this endpoint yet. The platform's configured timeouts for calls to your server are 10 seconds to connect and 30 seconds in total.

Verify ​

Run the conformance check with --only orders to check this endpoint against the fixtures.

Verify this endpoint

node tools/adserver-check/bin/adserver-check.mjs http://localhost:3000/api/v1 --key sk_test_local --only orders

No playground here: the request would go to your own server. Run the checker.

The checker is not published to npm yet. See how to run it.

Docs version 1.0.1