---
updatedAt: 2026-08-21T09:34:19.000Z
---

# Upload promotions file (S3)

See the [Promotions API Guides](https://api-docs.deliveroo.com/docs/overview-1) for prerequisites and the full upload lifecycle.

The Request a Presigned Upload URL endpoint returns a presigned `upload_url` that you use to upload your promotions file directly to S3. The `upload_url` is valid for a single file upload only and expires 1 hour after issue (see `expires_at` in the response).

**Do not route this request through the Deliveroo API gateway** — `PUT` directly to the `upload_url` returned by the previous step.

**No authentication header on the S3 PUT**

The presigned URL already contains AWS authentication parameters in its query string. Do not send the `Authorization: Bearer ...` header to S3 — S3 may reject the request.

**File format**

The body is a JSON object with a `promotions` array. Each entry is one promotion — a single mechanic and reward for a partner-controlled `promotion_id`. Each `promotion_id` must be unique within a file.

**File size limit**

Maximum file size: 50 MB. Files exceeding this limit will fail schema validation and the job will transition to `status: failed` with a `FILE_TOO_LARGE` entry in the status response.

**Sync semantics and duplicate detection**

Each upload is treated as the canonical state for the brand. Promotions absent from the new file are removed; only promotions whose content actually changed are updated. If the new file's `content_hash` matches your most recent successful upload for this `brand_id`, the job terminates with `status: failed` and `error_code: DUPLICATE_FILE` (no promotions are re-processed). The check compares only against your most recent successful upload, not the full history, so re-uploading an earlier file to revert to a prior state is allowed. Inspect the outcome via `GET /brands/{brand_id}/promotions/jobs/{job_id}/status`.

# OpenAPI definition

```json
{
  "openapi": "3.1.0",
  "x-readme": {
    "explorer-enabled": false
  },
  "info": {
    "title": "Promotions API (Beta)",
    "version": "1.0",
    "contact": {
      "name": "NV Partner Growth"
    },
    "description": "**Beta** — under active development. Endpoint shapes may change before release.\n\nExternal partner API for managing promotions on Deliveroo.\n\nSupports bulk promotion upload via presigned S3 URL, async single-promotion updates, and async job status polling.\n\nSee the [Promotions API Guides](https://api-docs.deliveroo.com/docs/overview-1) for prerequisites, OAuth setup, the end-to-end upload lifecycle, supported reward types, and error handling."
  },
  "servers": [
    {
      "url": "https://api.developers.deliveroo.com"
    }
  ],
  "security": [
    {
      "OAuth2": [
        "promotions"
      ]
    }
  ],
  "paths": {
    "/upload_url": {
      "put": {
        "summary": "Upload promotions file (S3)",
        "operationId": "promotions-v1-upload-url",
        "tags": [
          "Upload promotions file (S3)"
        ],
        "security": [],
        "description": "See the [Promotions API Guides](https://api-docs.deliveroo.com/docs/overview-1) for prerequisites and the full upload lifecycle.\n\nThe Request a Presigned Upload URL endpoint returns a presigned `upload_url` that you use to upload your promotions file directly to S3. The `upload_url` is valid for a single file upload only and expires 1 hour after issue (see `expires_at` in the response).\n\n**Do not route this request through the Deliveroo API gateway** — `PUT` directly to the `upload_url` returned by the previous step.\n\n**No authentication header on the S3 PUT**\n\nThe presigned URL already contains AWS authentication parameters in its query string. Do not send the `Authorization: Bearer ...` header to S3 — S3 may reject the request.\n\n**File format**\n\nThe body is a JSON object with a `promotions` array. Each entry is one promotion — a single mechanic and reward for a partner-controlled `promotion_id`. Each `promotion_id` must be unique within a file.\n\n**File size limit**\n\nMaximum file size: 50 MB. Files exceeding this limit will fail schema validation and the job will transition to `status: failed` with a `FILE_TOO_LARGE` entry in the status response.\n\n**Sync semantics and duplicate detection**\n\nEach upload is treated as the canonical state for the brand. Promotions absent from the new file are removed; only promotions whose content actually changed are updated. If the new file's `content_hash` matches your most recent successful upload for this `brand_id`, the job terminates with `status: failed` and `error_code: DUPLICATE_FILE` (no promotions are re-processed). The check compares only against your most recent successful upload, not the full history, so re-uploading an earlier file to revert to a prior state is allowed. Inspect the outcome via `GET /brands/{brand_id}/promotions/jobs/{job_id}/status`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerPromotionsUpload"
              },
              "example": {
                "promotions": [
                  {
                    "promotion_id": "GROCER-PCT-OFF-PASTA-2026Q2",
                    "promotion_type": "PERCENTAGE_OFF_ON_ITEMS",
                    "name": "20% off selected pasta",
                    "user_target": "LOYALTY_CUSTOMER",
                    "sites": [
                      "grocer-gb-london-001",
                      "grocer-gb-london-002"
                    ],
                    "fulfillment_method": "DELIVERY",
                    "start_at": "2026-06-01T00:00:00Z",
                    "end_at": "2026-06-30T23:59:59Z",
                    "condition": {
                      "items": [
                        "5000169816246",
                        "5000169816247"
                      ]
                    },
                    "reward": {
                      "percentage": 20
                    }
                  },
                  {
                    "promotion_id": "GROCER-AMT-OFF-DAIRY-2026Q3",
                    "promotion_type": "AMOUNT_OFF_ON_ITEMS",
                    "name": "£1 off butter",
                    "user_target": "ALL_CUSTOMERS",
                    "sites": [
                      "grocer-gb-london-001"
                    ],
                    "fulfillment_method": "COLLECTION",
                    "start_at": "2026-07-01T00:00:00Z",
                    "end_at": "2026-07-31T23:59:59Z",
                    "condition": {
                      "items": [
                        "5000169821000"
                      ]
                    },
                    "reward": {
                      "amount_off": 100
                    }
                  },
                  {
                    "promotion_id": "GROCER-SECOND-ITEM-2026Q2",
                    "promotion_type": "PERCENTAGE_OFF_ON_SECOND_ITEM",
                    "name": "50% off second item",
                    "user_target": "LOYALTY_CUSTOMER",
                    "sites": [
                      "grocer-gb-london-001"
                    ],
                    "fulfillment_method": "DELIVERY",
                    "start_at": "2026-06-01T00:00:00Z",
                    "end_at": "2026-06-30T23:59:59Z",
                    "condition": {
                      "items": [
                        "5000169830001",
                        "5000169830002"
                      ],
                      "quantity": 2
                    },
                    "reward": {
                      "percentage": 50
                    }
                  },
                  {
                    "promotion_id": "GROCER-BUY3-PASTA-PCT-2026Q3",
                    "promotion_type": "PERCENTAGE_OFF_ON_SINGLE_ITEM_MULTIBUY",
                    "name": "Buy 3 pasta, 30% off",
                    "user_target": "ALL_CUSTOMERS",
                    "sites": [
                      "grocer-gb-london-001"
                    ],
                    "fulfillment_method": "DELIVERY",
                    "start_at": "2026-07-01T00:00:00Z",
                    "end_at": "2026-07-31T23:59:59Z",
                    "condition": {
                      "items": [
                        "5000169816246"
                      ],
                      "quantity": 3
                    },
                    "reward": {
                      "percentage": 30
                    }
                  },
                  {
                    "promotion_id": "GROCER-SANDWICH-2FOR2-2026Q2",
                    "promotion_type": "FIXED_PRICE_ON_SINGLE_ITEM_MULTIBUY",
                    "name": "2 sandwiches for £2",
                    "user_target": "ALL_CUSTOMERS",
                    "sites": [
                      "grocer-gb-london-001",
                      "grocer-gb-london-002"
                    ],
                    "fulfillment_method": "DELIVERY",
                    "start_at": "2026-06-10T00:00:00Z",
                    "end_at": "2026-06-25T23:59:59Z",
                    "condition": {
                      "items": [
                        "5000169880001"
                      ],
                      "quantity": 2
                    },
                    "reward": {
                      "fixed_price": 200
                    }
                  },
                  {
                    "promotion_id": "GROCER-MIX3-SNACKS-25PCT-2026Q3",
                    "promotion_type": "PERCENTAGE_OFF_ON_MULTIPLE_ITEM_MULTIBUY",
                    "name": "Any 3 snacks, 25% off",
                    "user_target": "ALL_CUSTOMERS",
                    "sites": [
                      "grocer-gb-london-001"
                    ],
                    "fulfillment_method": "DELIVERY",
                    "start_at": "2026-07-01T00:00:00Z",
                    "end_at": "2026-07-31T23:59:59Z",
                    "condition": {
                      "items": [
                        "5000169816101",
                        "5000169816102",
                        "5000169816103"
                      ],
                      "quantity": 3
                    },
                    "reward": {
                      "percentage": 25
                    }
                  },
                  {
                    "promotion_id": "GROCER-ANY2-DRINKS-4-2026Q3",
                    "promotion_type": "FIXED_PRICE_ON_MULTIPLE_ITEM_MULTIBUY",
                    "name": "Any 2 drinks for £4",
                    "user_target": "ALL_CUSTOMERS",
                    "sites": [
                      "grocer-gb-london-001"
                    ],
                    "fulfillment_method": "DELIVERY",
                    "start_at": "2026-07-01T00:00:00Z",
                    "end_at": "2026-07-31T23:59:59Z",
                    "condition": {
                      "items": [
                        "5000169816201",
                        "5000169816202",
                        "5000169816203",
                        "5000169816204"
                      ],
                      "quantity": 2
                    },
                    "reward": {
                      "fixed_price": 400
                    }
                  },
                  {
                    "promotion_id": "GROCER-BASKET-10PCT-2026Q3",
                    "promotion_type": "PERCENTAGE_OFF_ON_BASKET",
                    "name": "10% off £20 baskets",
                    "user_target": "ALL_CUSTOMERS",
                    "sites": [
                      "grocer-gb-london-001"
                    ],
                    "fulfillment_method": "DELIVERY",
                    "start_at": "2026-07-01T00:00:00Z",
                    "end_at": "2026-07-31T23:59:59Z",
                    "condition": {
                      "min_order_value": 2000,
                      "alcohol_allowed": false
                    },
                    "reward": {
                      "percentage": 10,
                      "max_discount": 500
                    }
                  },
                  {
                    "promotion_id": "GROCER-FREE-DELIVERY-WEEKEND-2026Q3",
                    "promotion_type": "FREE_DELIVERY",
                    "name": "Free delivery weekend",
                    "user_target": "ALL_CUSTOMERS",
                    "sites": [
                      "grocer-gb-london-001"
                    ],
                    "fulfillment_method": "DELIVERY",
                    "start_at": "2026-07-04T00:00:00Z",
                    "end_at": "2026-07-06T23:59:59Z",
                    "condition": {
                      "min_order_value": 1500,
                      "alcohol_allowed": false
                    },
                    "reward": {}
                  },
                  {
                    "promotion_id": "GROCER-BUY2-GET1-FREE-2026Q3",
                    "promotion_type": "BUY_X_FOR_Y",
                    "name": "Buy 2 get 1 free",
                    "user_target": "ALL_CUSTOMERS",
                    "sites": [
                      "grocer-gb-london-001"
                    ],
                    "fulfillment_method": "DELIVERY",
                    "start_at": "2026-07-01T00:00:00Z",
                    "end_at": "2026-07-31T23:59:59Z",
                    "condition": {
                      "items": [
                        "5000169816301"
                      ],
                      "quantity": 2
                    },
                    "reward": {
                      "quantity": 1
                    }
                  },
                  {
                    "promotion_id": "GROCER-BUY3PLUS-SAVE15-2026Q3",
                    "promotion_type": "BUY_X_PLUS_SAVE_Y_PERCENT",
                    "name": "Buy 3 or more, save 15%",
                    "user_target": "ALL_CUSTOMERS",
                    "sites": [
                      "grocer-gb-london-001"
                    ],
                    "fulfillment_method": "DELIVERY",
                    "start_at": "2026-07-01T00:00:00Z",
                    "end_at": "2026-07-31T23:59:59Z",
                    "condition": {
                      "items": [
                        "5000169816401",
                        "5000169816402"
                      ],
                      "quantity": 3
                    },
                    "reward": {
                      "percentage": 15
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "File accepted by S3. Poll the `job_id` returned by the Request Upload URL endpoint to track processing."
          },
          "400": {
            "description": "Malformed request to S3 — check `Content-Type` and HTTP method."
          },
          "403": {
            "description": "Presigned URL expired or signature mismatch — request a new URL."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "OAuth2": {
        "type": "oauth2",
        "description": "OAuth2 client credentials. Obtain an access token from the token endpoint and pass it as `Authorization: Bearer <token>` on every request.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://auth.developers.deliveroo.com/oauth2/token",
            "scopes": {
              "promotions": "Read and write access to the Promotions API."
            }
          }
        }
      }
    },
    "schemas": {
      "PartnerPromotionsUpload": {
        "type": "object",
        "required": [
          "promotions"
        ],
        "description": "Canonical promotion set for the brand. Each entry in `promotions` is a single promotion — one mechanic and one reward for a partner-controlled `promotion_id`. Sync semantics: promotions absent from a subsequent upload are removed automatically. An empty array removes all promotions for this brand.",
        "properties": {
          "promotions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Promotion"
            }
          }
        }
      },
      "Promotion": {
        "type": "object",
        "required": [
          "promotion_id",
          "promotion_type",
          "sites",
          "start_at",
          "end_at",
          "condition",
          "reward"
        ],
        "properties": {
          "promotion_id": {
            "type": "string",
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9_.\\-]+$",
            "description": "Partner's external promotion identifier. Mandatory — the API does not auto-generate IDs. Unique within a file: one `promotion_id` = one mechanic = one reward. Max 128 chars, ASCII printable (`A–Z`, `a–z`, `0–9`, `_`, `-`, `.`).",
            "example": "GROCER-LOYALTY-PASTA-2026Q2"
          },
          "promotion_type": {
            "type": "string",
            "description": "The promotion mechanic. Determines which `condition` and `reward` sub-fields are required (see those schemas). A single-item multibuy triggers when the customer buys `condition.quantity` units of the same item; a multiple-item multibuy triggers when the customer assembles a bundle of `condition.quantity` units across different items from `condition.items`.",
            "enum": [
              "PERCENTAGE_OFF_ON_ITEMS",
              "AMOUNT_OFF_ON_ITEMS",
              "PERCENTAGE_OFF_ON_BASKET",
              "PERCENTAGE_OFF_ON_SECOND_ITEM",
              "PERCENTAGE_OFF_ON_SINGLE_ITEM_MULTIBUY",
              "FIXED_PRICE_ON_SINGLE_ITEM_MULTIBUY",
              "PERCENTAGE_OFF_ON_MULTIPLE_ITEM_MULTIBUY",
              "FIXED_PRICE_ON_MULTIPLE_ITEM_MULTIBUY",
              "FREE_DELIVERY",
              "BUY_X_FOR_Y",
              "BUY_X_PLUS_SAVE_Y_PERCENT"
            ]
          },
          "name": {
            "type": "string",
            "description": "Human-readable promotion name (for partner-side reporting and reconciliation).",
            "example": "20% off selected pasta"
          },
          "user_target": {
            "type": "string",
            "description": "Customer segment. Defaults to `ALL_CUSTOMERS` if omitted. `LOYALTY_CUSTOMER` requires the partner's loyalty programme to be set up with Deliveroo — partners should consult their Account Manager before integrating loyalty promotions. `PLUS_SUBSCRIBER` is incompatible with `FREE_DELIVERY` (Plus already includes free delivery). Enforcement is handled by Deliveroo's consumer-side validation; no additional partner integration is required for loyalty gating.",
            "enum": [
              "ALL_CUSTOMERS",
              "NEW_CUSTOMER",
              "LAPSED_CUSTOMER",
              "LOYALTY_CUSTOMER",
              "PLUS_SUBSCRIBER",
              "STUDENT",
              "NEW_TO_BRAND"
            ]
          },
          "start_at": {
            "type": "string",
            "format": "date-time",
            "description": "Promotion start. Use explicit UTC (`Z`) or offset (e.g. `+01:00`). Inclusive — the promotion is active AT this timestamp."
          },
          "end_at": {
            "type": "string",
            "format": "date-time",
            "description": "Promotion end. Must be strictly greater than `start_at`. Inclusive — the promotion is active UNTIL AND INCLUDING this timestamp. No minimum lead time — partners may shrink an active promotion to end at the current time."
          },
          "sites": {
            "type": "array",
            "description": "Partner location IDs the promotion applies to. Must already be synced to Deliveroo. Each promotion carries its own `sites`.",
            "items": {
              "type": "string"
            },
            "example": [
              "grocer-gb-london-001",
              "grocer-gb-london-002"
            ]
          },
          "fulfillment_method": {
            "type": "string",
            "description": "Restricts the promotion to a specific order fulfillment channel. Defaults to `ANY`. Grocery partners typically use `DELIVERY` or `COLLECTION`.",
            "enum": [
              "ANY",
              "DELIVERY",
              "COLLECTION"
            ],
            "default": "ANY"
          },
          "condition": {
            "$ref": "#/components/schemas/Condition"
          },
          "reward": {
            "$ref": "#/components/schemas/Reward"
          }
        }
      },
      "Condition": {
        "type": "object",
        "description": "What the basket must satisfy for the promotion to apply. Field requirements depend on `promotion_type`:\n* `items` is required for every item-level type (all types except `PERCENTAGE_OFF_ON_BASKET` and `FREE_DELIVERY`).\n* `quantity` is required on `PERCENTAGE_OFF_ON_SECOND_ITEM`, every `*_MULTIBUY` type, and the `BUY_X_*` types — it is the qualifying \"buy `N`\" count the basket must contain.\n* `min_order_value` and `alcohol_allowed` are valid on `PERCENTAGE_OFF_ON_BASKET` and `FREE_DELIVERY` only.\n\nCurrency for `min_order_value` is derived from the `brand_id` (a brand is bound to a single market at onboarding). Values are in minor currency units (pence for GBP, cents for EUR, etc.).",
        "properties": {
          "items": {
            "type": "array",
            "description": "Partner item identifiers (EAN / SKU / UPC) the condition applies to. Must already exist in Deliveroo's catalogue for the brand. Required for item-level types; omit for basket (`PERCENTAGE_OFF_ON_BASKET`) and delivery (`FREE_DELIVERY`) promotions. Max 2,000 items per `promotion_id`.",
            "items": {
              "type": "string"
            },
            "example": [
              "5000169816246",
              "5000169816247"
            ]
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "description": "Quantity of qualifying items the basket must contain — the `N` in \"buy `N` for £Y\" or \"buy `N`, X% off\". Required on `PERCENTAGE_OFF_ON_SECOND_ITEM`, every `*_MULTIBUY` type, and the `BUY_X_*` types.\n\nMinimum is `2` for every type above **except** `FIXED_PRICE_ON_SINGLE_ITEM_MULTIBUY`, which also accepts `1` (a single qualifying item at the fixed price)."
          },
          "min_order_value": {
            "type": "integer",
            "minimum": 1,
            "description": "Minimum basket subtotal in minor currency units before the reward applies. Valid on `PERCENTAGE_OFF_ON_BASKET` and `FREE_DELIVERY` only."
          },
          "alcohol_allowed": {
            "type": "boolean",
            "default": false,
            "description": "Whether the reward may apply to baskets containing alcohol. Valid on `PERCENTAGE_OFF_ON_BASKET` and `FREE_DELIVERY` only."
          }
        }
      },
      "Reward": {
        "type": "object",
        "description": "What the customer gets. Field requirements depend on `promotion_type`:\n* `percentage` is required on every `PERCENTAGE_*` type and on `BUY_X_PLUS_SAVE_Y_PERCENT`.\n* `amount_off` is required on every `AMOUNT_*` type.\n* `fixed_price` is required on every `FIXED_PRICE_*` type.\n* `quantity` is the reward-side quantity — the number of items received, required on `BUY_X_FOR_Y`.\n* `max_discount` is valid on `PERCENTAGE_*` types only.\n* `FREE_DELIVERY` takes no reward fields — send an empty object (`{}`).\n\nCurrency for `amount_off` / `fixed_price` / `max_discount` is derived from the `brand_id` (a brand is bound to a single market at onboarding). Values are in minor currency units (pence for GBP, cents for EUR, etc.).",
        "properties": {
          "percentage": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Integer percent (`20` = 20%). Required on every `PERCENTAGE_*` type and on `BUY_X_PLUS_SAVE_Y_PERCENT`."
          },
          "amount_off": {
            "type": "integer",
            "minimum": 1,
            "description": "Amount in minor currency units (pence / cents). `200` = £2.00. Required on every `AMOUNT_*` type."
          },
          "fixed_price": {
            "type": "integer",
            "minimum": 1,
            "description": "Bundle price in minor currency units. `400` = £4.00. Required on every `FIXED_PRICE_*` type."
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "description": "Reward-side quantity — the number of items the customer receives. Required on `BUY_X_FOR_Y` (the `Y` in \"buy `X` for `Y`\")."
          },
          "max_discount": {
            "type": "integer",
            "minimum": 1,
            "description": "Caps the absolute discount in minor currency units. Valid on `PERCENTAGE_*` types only."
          }
        }
      }
    }
  }
}
```