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

# Buyer RFPs

> Draft the ask you want to put to the market: an RFP per advertiser, with flight dates, a budget range for proposals, channels and geo, all optional.

A buyer **RFP** is the standing ask you put to the market: what you want, for which
advertiser, so sellers can answer with proposals. It belongs to one advertiser,
never to an agency as a whole, and it stays on record so you can come back to it,
change it while it is a draft, or copy it into your next one. It is not the seller-side
RFP record described in [Seller RFP lifecycle](https://docs.apostra.com/v3/rfp-lifecycle.md), and it is not a
proposal, which is a seller's answer.

This page covers what you can do with an RFP today: draft it, change it, copy it
and read it back. **Nothing is sent to a seller yet.** Opening an RFP to sellers
comes in a later release, and campaigns keep working as they do now.

> [!NOTE]
>
> Buyer RFPs are part of the buyer beta (the V4 beta feature profile). An account
> that is not on the beta is refused with `FEATURE_NOT_ENABLED` on every call that
> reads or writes an RFP, and an account whose profile cannot be read is refused
> with `SERVICE_UNAVAILABLE`, never let through. Your profile is described in
> [Accepting a plan sets what your account can do](https://docs.apostra.com/v3/plan-and-billing.md#accepting-a-plan-sets-what-your-account-can-do).

## What an RFP holds

Only the name is required. The thinner the ask, the vaguer the proposals that
will come back, so add what you know.

| Field | What it is |
| - | - |
| `name` | What you call the RFP, up to 200 characters. |
| `brief` | What you want, in prose, up to 8,000 characters. |
| `flight` | When you want to run: `start` and `end` dates (`YYYY-MM-DD`, `start` on or before `end`). This is the ask. It is not how long the RFP stays open. |
| `budgetRange` | The size of proposal you want back: `min`, `max` or both, with an ISO 4217 `currency`, at most two decimals. It describes the proposals you are asking for. It is not a pot the RFP holds, and it is not the budget of any campaign you later make. |
| `channels` | AdCP media channels, such as `olv`, `ctv` or `social`, with no repeats. |
| `geo` | `countries` as ISO 3166-1 codes (`US`) and `regions` as ISO 3166-2 codes (`US-CA`). Codes, never names. |

`flight`, `budgetRange`, `channels` and `geo` use AdCP's own shapes and names, so
they will go to a seller as the matching AdCP offer filters (`start_date` and
`end_date`, `budget_range`, `channels`, `countries` and `regions`) when sending
ships.

Every read returns the same names the save takes, plus these read-only fields:

| Field | What it is |
| - | - |
| `rfpId` | The RFP's own ID, `rfp_` followed by 21 letters and digits. It never changes. |
| `planId` | The plan the RFP belongs to (below). |
| `advertiserId` | The advertiser that owns it. |
| `status` | `draft`, `open` or `closed`. Every RFP you can make today is a `draft`. |
| `revision` | Counts changes. A change must name the revision it read. |
| `window` | Set once an RFP is opened: when it opened, when replies are due, when it closed. `null` for a draft. |
| `duplicatedFromRfpId` | The RFP this one was copied from, if any. |

## Every RFP belongs to a plan

A plan is one planning cycle for one advertiser. It is not a billing plan such as
Business Manager. When you create an RFP, Apostra makes the plan for you, so you
never have to name one. The plan's ID, `planId`,
is the AdCP `opportunity_id` that every request under the plan carries to a
seller, so a seller will see one conversation per plan. The ID is opaque and
never changes. Apostra does not yet let you put a second RFP into an existing
plan or name a plan yourself.

## Create a draft

Call `save_buyer_rfp` with `advertiserId`, `name` and an `idempotencyKey`. Add
any ask fields you have.

```json theme={null}
{
  "advertiserId": "1234",
  "name": "Q4 awareness",
  "flight": { "start": "2026-11-01", "end": "2026-12-31" },
  "budgetRange": { "min": 500, "max": 10000, "currency": "USD" },
  "channels": ["olv", "social"],
  "geo": { "countries": ["US"] },
  "idempotencyKey": "q4-awareness-create-0001"
}
```

The result names the saved RFP under `buyerRfp`, with `action: "created"`. The
`idempotencyKey` is yours to choose, 16 to 255 letters, digits and `_ . : -`. Send
the same key again and you get the same RFP back with `replayed: true`. Reuse a
key for a different request and the call is refused with a conflict.

## Change a draft

Call `save_buyer_rfp` with `rfpId`, the `expectedRevision` from your last read,
and the fields to change. A field you leave out stays. `null` clears an optional
field.

```json theme={null}
{
  "rfpId": "rfp_AbCdEfGhIjKlMnOpQrStU",
  "expectedRevision": 1,
  "brief": null,
  "budgetRange": { "max": 15000, "currency": "USD" }
}
```

* A stale `expectedRevision` is refused as a conflict that names the current
  revision. Read the RFP again and retry.
* Only a draft changes. An RFP that is open or closed refuses the change, and the
  message tells you to copy it.
* An update takes no `idempotencyKey`, no `advertiserId` and no
  `duplicateFromRfpId`: the revision is what makes a retry safe.

## Copy an RFP into a new draft

Call `save_buyer_rfp` with `duplicateFromRfpId` and an `idempotencyKey`. The copy
takes the source's ask, gets a new RFP ID, and **gets a new plan**, because a
re-run is a new planning cycle. Its name is the source's name with " (copy)", or
the `name` you send. The source is untouched. Duplicating takes no other ask
fields: copy first, then change the copy.

## Read RFPs back

* `get` with `kind: "buyer_rfp"` and the `id` of an RFP returns it.
* `search` with `kind: "buyer_rfp"` and `filter.advertiserId` lists that
  advertiser's RFPs, newest first, in pages. Narrow it with
  `filter.rfpStatus`. It takes no text query. When a page has more, it returns a
  `nextCursor`.

You can only read and change the RFPs of advertisers your account can reach. An
RFP that belongs to another account answers `NOT_FOUND`, the same as one that
does not exist.

## What is not here yet

* Opening an RFP to sellers, extending it, closing it and reopening it.
* Proposals arriving on an RFP, and turning them into campaigns.
* Naming a plan, or putting more than one RFP in a plan.

When these ship, this page will say how they work.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.