---
title: "Place an order (pay-first)"
method: POST
path: "/v1/orders"
tags: ["Orders"]
---

# Place an order (pay-first)

`POST /v1/orders`

## Request

- Base URL: `https://api.placementapi.com`
- URL: `https://api.placementapi.com/v1/orders`
- Auth: HTTP bearer

Debits credits immediately. Requires the `order` scope and an Idempotency-Key header. With quote_id the held price is charged instead of the live one and the freshness guard is bypassed; without it a stale catalogue price is refused. Content is decided now against what the listing allows: 'supplied' without inline `content` parks the order in awaiting_content for 14 days; 'papi_written' charges 1,500 credits with the order; inline content is validated first (422, nothing charged). Orders that carry an article are one placement each.

## Headers

- `Idempotency-Key` string, required

## Request body

- object
  - `offer_id` string, required: From /v1/offers or a quote line / alternative.
  - `quote_id` string: Redeem a held price from POST /v1/quote or POST /v1/targets/gap.
  - `target_url` string, uri, required: The page the link points at.
  - `anchor_text` string, nullable: Omit when order_requirements marks it optional (publisher writes the anchor).
  - `target_keyword` string, nullable: The query the page should rank for. Optional and worth sending: it seeds outcome tracking and anchor classification.
  - `target_market` string, nullable: ISO country for the SERP this is judged in. Defaults to US.
  - `target_device` 'desktop' | 'mobile' | 'null', nullable
  - `anchor_plan` AnchorPlan: Anchor text is a DISTRIBUTION, not a string. Many links with one exact-match anchor is the classic over-optimisation footprint. Mix branded, topical, generic and naked_url anchors.
    - `anchors` object[], required
      - `text` string, required
      - `type` 'branded' | 'naked_url' | 'generic' | 'topical' | 'partial_match' | 'exact_match', required
      - `weight` number: Relative weight; normalised across the plan.
  - `quantity` integer
  - `content_mode` 'publisher' | 'service' | 'supplied' | 'papi_written': Defaults from order_requirements.content: 'publisher' where the publisher writes, else 'supplied'. Refused (400 content_not_accepted) where the listing does not allow it.
  - `content_tier` string: For 'service': a vendor_label (or tier) from order_requirements.content.options. Defaults to the cheapest paid tier.
  - `if_no_content` 'convert_to_service' | 'cancel': What happens when supplied copy misses content.due_at.
  - `content` ContentBody: An article. Send body_markdown, body_html or doc_url. A doc_url alone cannot be validated and is accepted with a warning.
    - `title` string, nullable
    - `body_markdown` string, nullable
    - `body_html` string, nullable
    - `doc_url` string, uri, nullable
    - `meta_description` string, nullable

## Response `200`

Idempotent replay of an earlier order with the same key.

- object
  - `order_id` string
  - `replayed` true

## Other responses

- `201`: Placed.
- `400`: `idempotency_key_required`, `invalid_json`, `bad_request`, `content_not_accepted`, `no_service_tier`, `one_article_per_order`, `unknown_content_tier`, `content_not_expected`.
- `401`: Missing, malformed or revoked Bearer credential: `{ error: "unauthorized" }`.
- `402`: `insufficient_credits`, with unit_price_credits and content_price_credits.
- `403`: `insufficient_scope`, `customer_key_required`, or a ledger refusal: `per_order_cap_exceeded`, `daily_cap_exceeded`, `customer_inactive`, `customer_not_found`.
- `404`: `offer_not_found`, `quote_not_found`.
- `409`: `offer_delisted`, `offer_not_priceable`, `quote_expired` (with expires_at), `offer_not_in_quote`, `price_stale` (with observed_at).
- `422`: `content_invalid`: the inline article would not pass the publisher; `validation` lists the problems. Nothing was charged.
- `503`: `server_misconfigured` when the store is not configured; `upstream_unfunded` when the measurement provider's prepaid balance is exhausted (fresh profiles, gap maps and SERPs pause; cached results and catalogue are unaffected); `upstream_unavailable` on a provider error; `payments_unavailable` when card payments are not configured.

---

[API](https://skmtc.dev/placementapi/apis/placement-api.md) · [All operations](https://skmtc.dev/placementapi/apis/placement-api/llms.txt) · [OpenAPI document](https://skmtc.dev/placementapi/apis/placement-api/revisions/fc4e7866eaf7?raw)
