Retailer Billed

Introduction

📘

Commerce Max Retailer Billed introduces a buying model in which campaign budgets are funded by the retailer rather than the advertiser.

This guide covers the API changes required for third-party buying platforms to create and manage retailer-billed Sponsored Products campaigns end-to-end.

Business Context

Prior to this release, the Retail Media API did not expose retailer scoping on balances, campaigns, or line items. Retailer-billed buying requires platforms to associate balances, campaigns, and line items to the same retailer — the API now enforces these constraints explicitly.

Prerequisites

  • API version 2026-01 or later is required to see retailer-billed balances. On prior versions, retailer-billed balances are hidden by default to prevent integration surprises during rollout.
  • Platforms must use the supply account ID when querying balances if they wish to discover retailer-billed balances.

Key Concepts

  • Retailer-billed balance — a budget object funded by the retailer, scoped to a specific RetailerId. Cannot be created via API; must be retrieved via GET /balances.
  • RetailerId — the identifier of the retailer that funds the balance and scopes the campaign. Must be consistent across balance → campaign → line item.
  • budgetModel — a new field on the retailer search response indicating which budget models (e.g., retailerBilled, capped, uncapped) are supported at a given retailer.
⚠️

Backward compatibility for balance endpoints:

  • The poNumber field on balance responses is removed in 2026-01 and replaced by two separate fields: retailerPoNumber and criteoPoNumber. This is a breaking change for consumers of GET /balances on prior versions who rely on poNumber.
  • All other new fields (retailerId, privateMarketBillingType) are additive. Retailer-billed balances are hidden by default on prior API versions.

Endpoints Overview

📘

All endpoints changes related to Retailer Billed are also documented in the following pages:

VerbEndpointDescription
GET/retail-media/balances/{balanceId}Get a single balance. Now includes retailerId, retailerPoNumber, criteoPoNumber.
GET/retail-media/accounts/{accountId}/balancesList balances. Now includes retailer fields. Retailer-billed balances hidden on prior versions.
GET/retail-media/balances/{balanceId}/historyGet balance change history. Now includes retailerPoNumber, criteoPoNumber.
POST/retail-media/balances/{balanceId}/campaigns/appendAdd campaigns to a balance. Validates retailer consistency.
POST/retail-media/balances/{balanceId}/campaigns/deleteRemove campaigns from a balance. Returns error for retailer-billed balances.
POST/retail-media/accounts/{accountId}/campaignsCreate a campaign. Now accepts and validates retailerId.
GET/retail-media/campaignsList campaigns. Now returns retailerId; supports filtering by retailer.
GET/retail-media/campaigns/{campaignId}Get a campaign. Now returns retailerId.
POST/retail-media/campaigns/{campaignId}/auction-line-itemsCreate a line item. Enforces targetRetailerId matches campaign retailer.
POST/retail-media/accounts/{accountId}/retailers/searchSearch retailers. Now returns budgetModel in campaignAvailabilities.

Attributes

New and Changed Fields on Balances

Attribute

Data Type

Mutable

Description

retailerId

string?

init

Retailer this balance is scoped to.
Present only on retailer-billed balances.

Nullable? Y (null for non-retailer-billed)

retailerPoNumber

string?

always

Retailer purchase order number. Replaces the removed poNumber field.

Nullable? Y

criteoPoNumber

string?

always

Criteo purchase order number.
Replaces the removed poNumber field.

Nullable? Y

privateMarketBillingType

enum

init

Billing type for Private Market. Values: NotApplicable, BillByRetailer, BillByCriteo, Unknown

poNumber

string

Removed in 2026-01. Replaced by retailerPoNumber and criteoPoNumber.


New Field on Campaigns

A new field RetailerId has been added to the following endpoints:

  • /accounts/{accountId}/campaigns
  • /campaigns/{campaignId}

Attribute

Data Type

Description

RetailerId

string

The retailer this campaign is associated with.
Required when using a retailer-billed balance.

Writeable? Y (at create)
Nullable? Y (for non-retailer-billed campaigns)

New Fields on Retailer Search

The new fields are added to the following endpoint:

  • /accounts/{accountId}/retailers/search

Attribute

Data Type

Description

budgetModel

string

Budget model(s) supported for the given buyType / campaignType combination at this retailer.

Values: capped, uncapped, retailerBilled.

Writeable? N
Nullable? N

New Error Codes

Error codeEndpointMeaning
RetailerBilledBalanceImmutableDELETE /balances/{balanceId}/campaignsCannot remove a retailer-billed balance from a campaign.
RetailerMismatchWithBalancePOST /accounts/{accountId}/campaignsCampaign RetailerId does not match the balance's RetailerId.
RetailerMismatchWithCampaignPOST /campaigns/{campaignId}/auction-line-itemsLine item targetRetailerId does not match the campaign's RetailerId.

Endpoint Changes

Get List of Balances for an Account

This endpoint returns a paginated list of balances for an account.

On API version 2026-01 and later, retailer-billed balances are included and retailerId, retailerPoNumber, and criteoPoNumber are returned. The old poNumber field is removed.

📘

Query parameters

  • offset (int, default 0),
  • limit (int, default 25, max 500),
  • limitToId (List<string>)
https://api.criteo.com/2026-01/retail-media/accounts/{accountId}/balances

Sample Request

curl -L -X GET 'https://api.criteo.com/2026-01/retail-media/accounts/625702934721171442/balances' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <MY_ACCESS_TOKEN>'
import http.client
conn = http.client.HTTPSConnection("api.criteo.com")
headers = {'Accept': 'application/json', 'Authorization': 'Bearer <TOKEN>'}
conn.request("GET", "/2026-01/retail-media/accounts/625702934721171442/balances", headers=headers)
res = conn.getresponse()
print(res.read().decode("utf-8"))
OkHttpClient client = new OkHttpClient().newBuilder().build();
Request request = new Request.Builder()
  .url("https://api.criteo.com/2026-01/retail-media/accounts/625702934721171442/balances")
  .method("GET", null)
  .addHeader("Accept", "application/json")
  .addHeader("Authorization", "Bearer <TOKEN>")
  .build();
Response response = client.newCall(request).execute();
<?php
require_once 'HTTP/Request2.php';
$request = new HTTP_Request2();
$request->setUrl('https://api.criteo.com/2026-01/retail-media/accounts/625702934721171442/balances');
$request->setMethod(HTTP_Request2::METHOD_GET);
$request->setHeader(array('Accept' => 'application/json', 'Authorization' => 'Bearer <TOKEN>'));
echo $request->send()->getBody();

Sample Response

{
  "meta": { "count": 2, "offset": 0, "limit": 25 },
  "data": [
    {
      "id": "814283465675018240",
      "type": "Balance",
      "attributes": {
        "name": "Retailer Billed Q2 Budget",
        "retailerId": "1298",
        "retailerPoNumber": "A-5678",
        "criteoPoNumber": null,
        "memo": "Q2 retailer-billed campaign",
        "deposited": 2000.00,
        "spent": 0.00,
        "remaining": 2000.00,
        "startDate": "2026-07-01",
        "endDate": "2026-12-31",
        "status": "active",
        "balanceType": "capped",
        "spendType": "Onsite",
        "privateMarketBillingType": "billByRetailer",
        "createdAt": "2026-02-24T23:51:46+00:00",
        "updatedAt": "2026-02-24T23:51:48+00:00"
      }
    },
    {
      "id": "814313002113232896",
      "type": "Balance",
      "attributes": {
        "name": "Standard Advertiser Balance",
        "retailerId": null,
        "retailerPoNumber": null,
        "criteoPoNumber": "A-1234",
        "deposited": 2000.00,
        "spent": 0.00,
        "remaining": 2000.00,
        "startDate": "2026-07-01",
        "endDate": "2026-12-31",
        "status": "active",
        "balanceType": "capped",
        "spendType": "Onsite",
        "privateMarketBillingType": "billByCriteo",
        "createdAt": "2026-02-25T01:49:08+00:00",
        "updatedAt": "2026-02-25T01:49:10+00:00"
      }
    }
  ],
  "warnings": [],
  "errors": []
}

Add Campaigns to a Balance

This endpoint adds one or more campaigns to a balance. It validates that campaigns are retailer-native when the balance is retailer-billed.

https://api.criteo.com/2026-01/retail-media/balances/{balanceId}/campaigns/append

Sample Request

curl -L -X POST 'https://api.criteo.com/2026-01/retail-media/balances/814886589256347648/campaigns/append' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <MY_ACCESS_TOKEN>' \
-d '{
  "data": {
    "attributes": {
      "ids": ["718038552188952576", "234105251251242423"]
    },
    "type": "AppendCampaignsRequest"
  }
}'

Sample Response

{
  "data": {
    "attributes": {
      "ids": ["718038552188952576", "234105251251242423"]
    },
    "type": "BalanceCampaigns"
  },
  "warnings": [],
  "errors": []
}

Sample Response — Error (non-retailer-native campaign)

{
  "errors": [
    {
      "message": "Only retailer-sold campaigns are allowed to be mapped to a retailer-billed balance.",
      "status": 400
    }
  ]
}

Remove Campaign(s) from a Balance

This endpoint removes one or more campaigns from a balance.

https://api.criteo.com/2026-01/retail-media/balances/{balanceId}/campaigns/delete

Sample Request

curl -L -X POST 'https://api.criteo.com/2026-01/retail-media/balances/814886589256347648/campaigns/delete' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <MY_ACCESS_TOKEN>' \
-d '{
  "data": {
    "attributes": {
      "ids": ["234105251251242423"]
    },
    "type": "DeleteCampaignsRequest"
  }
}'

Sample Response

{
  "data": {
    "attributes": {
      "ids": ["718038552188952576"]
    },
    "type": "BalanceCampaigns"
  },
  "warnings": [],
  "errors": []
}

Create New Campaigns

This endpoint creates a new campaign.

⚠️

For retailer-billed campaigns, retailerId is required and must match the retailerId on the drawable balance.

https://api.criteo.com/2026-01/retail-media/accounts/{accountId}/campaigns

Sample Request

curl -L -X POST 'https://api.criteo.com/2026-01/retail-media/accounts/625702934721171442/campaigns' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <MY_ACCESS_TOKEN>' \
-d '{
  "data": {
    "type": "Campaign",
    "attributes": {
      "name": "Retailer Billed Campaign Q2",
      "startDate": "2026-07-01T00:00:00+00:00",
      "clickAttributionWindow": "30D",
      "viewAttributionWindow": "None",
      "retailerId": "1298",
      "drawableBalanceIds": ["814886589256347648"]
    }
  }
}'
import http.client
import json
conn = http.client.HTTPSConnection("api.criteo.com")
payload = json.dumps({
  "data": {
    "type": "Campaign",
    "attributes": {
      "name": "Retailer Billed Campaign Q2",
      "startDate": "2026-07-01T00:00:00+00:00",
      "clickAttributionWindow": "30D",
      "viewAttributionWindow": "None",
      "retailerId": "1298",
      "drawableBalanceIds": ["814886589256347648"]
    }
  }
})
headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer <TOKEN>'}
conn.request("POST", "/2026-01/retail-media/accounts/625702934721171442/campaigns", payload, headers)
print(conn.getresponse().read().decode("utf-8"))
OkHttpClient client = new OkHttpClient().newBuilder().build();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\"data\":{\"type\":\"Campaign\",\"attributes\":{\"name\":\"Retailer Billed Campaign Q2\",\"startDate\":\"2026-07-01T00:00:00+00:00\",\"clickAttributionWindow\":\"30D\",\"viewAttributionWindow\":\"None\",\"retailerId\":\"1298\",\"drawableBalanceIds\":[\"814886589256347648\"]}}}");
Request request = new Request.Builder()
  .url("https://api.criteo.com/2026-01/retail-media/accounts/625702934721171442/campaigns")
  .method("POST", body)
  .addHeader("Content-Type", "application/json")
  .addHeader("Accept", "application/json")
  .addHeader("Authorization", "Bearer <TOKEN>")
  .build();
Response response = client.newCall(request).execute();
<?php
require_once 'HTTP/Request2.php';
$request = new HTTP_Request2();
$request->setUrl('https://api.criteo.com/2026-01/retail-media/accounts/625702934721171442/campaigns');
$request->setMethod(HTTP_Request2::METHOD_POST);
$request->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json', 'Authorization' => 'Bearer <TOKEN>'));
$request->setBody('{"data":{"type":"Campaign","attributes":{"name":"Retailer Billed Campaign Q2","startDate":"2026-07-01T00:00:00+00:00","clickAttributionWindow":"30D","viewAttributionWindow":"None","retailerId":"1298","drawableBalanceIds":["814886589256347648"]}}}');
echo $request->send()->getBody();

Sample Response

{
  "data": {
    "id": "718038552188952576",
    "type": "Campaign",
    "attributes": {
      "name": "Retailer Billed Campaign Q2",
      "accountId": "625702934721171442",
      "type": "auction",
      "status": "inactive",
      "retailerId": "1298",
      "drawableBalanceIds": ["814886589256347648"],
      "budget": null,
      "budgetSpent": 0.0,
      "budgetRemaining": null,
      "startDate": "2026-07-01T00:00:00+00:00",
      "endDate": null,
      "clickAttributionWindow": "30D",
      "viewAttributionWindow": "None",
      "createdAt": "2026-05-08T00:00:00+00:00",
      "updatedAt": "2026-05-08T00:00:00+00:00"
    }
  }
}

Get Campaigns by Account ID and Campaign ID

Both endpoints now return retailerId in the campaign attributes.

The list endpoint supports filtering by retailerId.

https://api.criteo.com/2026-01/retail-media/accounts/{accountId}/campaigns?pageIndex=0&pageSize=25
https://api.criteo.com/2026-01/retail-media/campaigns/{campaignId}

Sample Request — Get a Single Campaign

curl -L -X GET 'https://api.criteo.com/2026-01/retail-media/campaigns/718038552188952576' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <MY_ACCESS_TOKEN>'

Sample Response

{
  "data": {
    "id": "718038552188952576",
    "type": "Campaign",
    "attributes": {
      "name": "Retailer Billed Campaign Q2",
      "accountId": "625702934721171442",
      "type": "auction",
      "status": "active",
      "retailerId": "1298",
      "drawableBalanceIds": ["814886589256347648"],
      "budget": null,
      "budgetSpent": 1250.00,
      "budgetRemaining": null,
      "startDate": "2026-07-01T00:00:00+00:00",
      "endDate": null,
      "clickAttributionWindow": "30D",
      "viewAttributionWindow": "None",
      "createdAt": "2026-05-08T00:00:00+00:00",
      "updatedAt": "2026-07-01T00:00:00+00:00"
    }
  }
}

Create an Auction Line Item

Creates an auction line item.

For retailer-billed campaigns, targetRetailerId is required and must match the campaign's retailerId. Only one retailer is allowed per retailer-billed campaign.

https://api.criteo.com/2026-01/retail-media/campaigns/{campaignId}/auction-line-items

Sample Request

curl -L -X POST 'https://api.criteo.com/2026-01/retail-media/campaigns/718038552188952576/auction-line-items' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <MY_ACCESS_TOKEN>' \
-d '{
  "data": {
    "type": "SponsoredProductsLineItem",
    "attributes": {
      "name": "Retailer Billed Line Item",
      "targetRetailerId": "1298",
      "startDate": "2026-07-01",
      "bidStrategy": "automated",
      "optimizationStrategy": "conversion",
      "keywordStrategy": "genericAndBranded"
    }
  }
}'
import http.client
import json
conn = http.client.HTTPSConnection("api.criteo.com")
payload = json.dumps({
  "data": {
    "type": "SponsoredProductsLineItem",
    "attributes": {
      "name": "Retailer Billed Line Item",
      "targetRetailerId": "1298",
      "startDate": "2026-07-01",
      "bidStrategy": "automated",
      "optimizationStrategy": "conversion",
      "keywordStrategy": "genericAndBranded"
    }
  }
})
headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer <TOKEN>'}
conn.request("POST", "/2026-01/retail-media/campaigns/718038552188952576/auction-line-items", payload, headers)
print(conn.getresponse().read().decode("utf-8"))
OkHttpClient client = new OkHttpClient().newBuilder().build();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\"data\":{\"type\":\"SponsoredProductsLineItem\",\"attributes\":{\"name\":\"Retailer Billed Line Item\",\"targetRetailerId\":\"1298\",\"startDate\":\"2026-07-01\",\"bidStrategy\":\"automated\",\"optimizationStrategy\":\"conversion\",\"keywordStrategy\":\"genericAndBranded\"}}}");
Request request = new Request.Builder()
  .url("https://api.criteo.com/2026-01/retail-media/campaigns/718038552188952576/auction-line-items")
  .method("POST", body)
  .addHeader("Content-Type", "application/json")
  .addHeader("Accept", "application/json")
  .addHeader("Authorization", "Bearer <TOKEN>")
  .build();
Response response = client.newCall(request).execute();
<?php
require_once 'HTTP/Request2.php';
$request = new HTTP_Request2();
$request->setUrl('https://api.criteo.com/2026-01/retail-media/campaigns/718038552188952576/auction-line-items');
$request->setMethod(HTTP_Request2::METHOD_POST);
$request->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json', 'Authorization' => 'Bearer <TOKEN>'));
$request->setBody('{"data":{"type":"SponsoredProductsLineItem","attributes":{"name":"Retailer Billed Line Item","targetRetailerId":"1298","startDate":"2026-07-01","bidStrategy":"automated","optimizationStrategy":"conversion","keywordStrategy":"genericAndBranded"}}}');
echo $request->send()->getBody();

Sample Response

{
  "data": {
    "id": "234105251251242423",
    "type": "SponsoredProductsLineItem",
    "attributes": {
      "name": "Retailer Billed Line Item",
      "campaignId": "718038552188952576",
      "targetRetailerId": "1298",
      "startDate": "2026-07-01T00:00:00+00:00",
      "endDate": null,
      "status": "inactive",
      "budget": null,
      "budgetSpent": 0.0,
      "budgetRemaining": null,
      "targetBid": 0.3,
      "maxBid": null,
      "monthlyPacing": null,
      "dailyPacing": null,
      "isAutoDailyPacing": false,
      "bidStrategy": "automated",
      "optimizationStrategy": "conversion",
      "keywordStrategy": "genericAndBranded",
      "flightSchedule": null,
      "createdAt": "2026-05-08T00:00:00+00:00",
      "updatedAt": "2026-05-08T00:00:00+00:00"
    }
  },
  "warnings": [],
  "errors": []
}

Sample Response — Error (retailer mismatch)

{
  "errors": [
    {
      "message": "The line item targetRetailerId must match the campaign retailerId.",
      "status": 400
    }
  ]
}

Search Retailer for an Account

This endpoint allows searching for available retailers for an account.

The campaignAvailabilities object now includes budgetModel, allowing platforms to determine which budget models are supported at a given retailer before creating a campaign.

https://api.criteo.com/2026-01/retail-media/accounts/{accountId}/retailers/search

Sample Request

curl -L -X POST 'https://api.criteo.com/2026-01/retail-media/accounts/625702934721171442/retailers/search' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <MY_ACCESS_TOKEN>' \
-d '{}'

Sample Response

{
  "data": [
    {
      "id": "retailer-789",
      "type": "Retailer",
      "attributes": {
        "name": "Example Retailer",
        "campaignAvailabilities": [
          {
            "buyType": "auction",
            "campaignType": "sponsoredProducts",
            "isAvailable": true,
            "budgetModel": "retailerBilled",
            "validCombinations": [
              {
                "pageType": "search",
                "pageEnvironmentType": "offsite"
              }
            ]
          }
        ]
      }
    }
  ]
}

Responses

ResponseTitleDetailTroubleshooting
🟢 200SuccessRequest executed successfully.
🔴 400Retailer-billed balance mapping"Only retailer-sold campaigns are allowed to be mapped to a retailer-billed balance."Only campaigns scoped to the same retailer as the balance can be appended via /campaigns/append.
🔴 400RetailerMismatchWithBalanceCampaign retailerId does not match the balance retailerId.Ensure the retailerId in campaign settings matches the retailerId on the balance you are associating.
🔴 400RetailerMismatchWithCampaignLine item targetRetailerId does not match the campaign retailerId.Ensure targetRetailerId on the line item matches the retailerId set on the parent campaign.
🔴 400Error deserializing requestA required field is missing or has an invalid value.Review the request body against the attributes table above.
🔴 403UnauthorizedVerify your access token and that your account has access to the retailer in question.