placementapi-com Orders API

Place, list, cancel and follow orders. Requires the `order` scope to spend.

Operations 4

POST /v1/orders Place an order (pay-first) #
GET /v1/orders Your orders #
POST /v1/orders/{order_id}/cancel Cancel before it reaches a supplier #
GET /v1/orders/{order_id}/outcome Everything observed since one placement #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/placementapi-com:placementapi-com-orders-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

placementapi-com-orders-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Placement Orders API
  version: 1.0.0
  description: 'Normalised backlink placement inventory, deduplicated across multiple suppliers, with independent verification, held quotes, ordering, content handling and post-placement outcome tracking. Every price is the buyer price in credits (1 credit = 1 US cent), already normalised from supplier currencies, so the figure you plan with is the figure you are charged. Supplier identity is never exposed.


    Authenticate with a `pk_live_…` API key as a Bearer token. Keys carry scopes: `read` (catalogue, quotes, account reads) and `order` (anything that moves money). An MCP server exposing the same operations as tools is available at https://api.placementapi.com/mcp with the same Bearer key; the tools call these endpoints, so pricing and verification are identical either way. Unknown query parameters and brief fields are refused with 400 rather than ignored.'
  contact:
    email: hello@placementapi.com
  termsOfService: https://placementapi.com/terms
servers:
- url: https://api.placementapi.com
security:
- apiKey: []
tags:
- name: Orders
  description: Place, list, cancel and follow orders. Requires the `order` scope to spend.
paths:
  /v1/orders:
    post:
      tags:
      - Orders
      operationId: place_order
      summary: Place an order (pay-first)
      description: '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.'
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - offer_id
              - target_url
              properties:
                offer_id:
                  type: string
                  description: From /v1/offers or a quote line / alternative.
                quote_id:
                  type: string
                  description: Redeem a held price from POST /v1/quote or POST /v1/targets/gap.
                target_url:
                  type: string
                  format: uri
                  description: The page the link points at.
                anchor_text:
                  type:
                  - string
                  - 'null'
                  minLength: 1
                  maxLength: 300
                  description: Omit when order_requirements marks it optional (publisher writes the anchor).
                target_keyword:
                  type:
                  - string
                  - 'null'
                  maxLength: 200
                  description: 'The query the page should rank for. Optional and worth sending: it seeds outcome tracking and anchor classification.'
                target_market:
                  type:
                  - string
                  - 'null'
                  minLength: 2
                  maxLength: 2
                  description: ISO country for the SERP this is judged in. Defaults to US.
                target_device:
                  type:
                  - string
                  - 'null'
                  enum:
                  - desktop
                  - mobile
                  - null
                anchor_plan:
                  $ref: '#/components/schemas/AnchorPlan'
                quantity:
                  type: integer
                  minimum: 1
                  maximum: 100
                  default: 1
                content_mode:
                  type: string
                  enum:
                  - publisher
                  - service
                  - supplied
                  - papi_written
                  description: '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:
                  type: string
                  maxLength: 80
                  description: 'For ''service'': a vendor_label (or tier) from order_requirements.content.options. Defaults to the cheapest paid tier.'
                if_no_content:
                  type: string
                  enum:
                  - convert_to_service
                  - cancel
                  default: convert_to_service
                  description: What happens when supplied copy misses content.due_at.
                content:
                  $ref: '#/components/schemas/ContentBody'
      responses:
        '200':
          description: Idempotent replay of an earlier order with the same key.
          content:
            application/json:
              schema:
                type: object
                properties:
                  order_id:
                    type: string
                  replayed:
                    type: boolean
                    const: true
                additionalProperties: true
        '201':
          description: Placed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  order_id:
                    type: string
                  replayed:
                    type: boolean
                    const: false
                  status:
                    type: string
                    enum:
                    - ordered
                    - awaiting_content
                  unit_price_credits:
                    type: integer
                  total_credits:
                    type: integer
                    description: unit price x quantity + content price.
                  content:
                    type: object
                    properties:
                      mode:
                        type: string
                        enum:
                        - publisher
                        - service
                        - supplied
                        - papi_written
                      tier:
                        type:
                        - string
                        - 'null'
                      price_credits:
                        type: integer
                      due_at:
                        type:
                        - string
                        - 'null'
                      if_no_content:
                        type:
                        - string
                        - 'null'
                      fallback_price_credits:
                        type:
                        - integer
                        - 'null'
                      submit:
                        type:
                        - string
                        - 'null'
                        description: POST /v1/orders/{order_id}/content when the buyer owes the article.
                      spec:
                        $ref: '#/components/schemas/ContentSpec'
                      validation:
                        oneOf:
                        - $ref: '#/components/schemas/ContentValidation'
                        - type: 'null'
                  inventory_mode:
                    $ref: '#/components/schemas/InventoryMode'
                  site_id:
                    type:
                    - string
                    - 'null'
                  note:
                    type: string
                    description: 'Present on blind and pre_approval inventory: says plainly that no specific domain was sold.'
                  requires_approval:
                    type: boolean
                    description: True on pre_approval inventory.
        '400':
          description: '`idempotency_key_required`, `invalid_json`, `bad_request`, `content_not_accepted`, `no_service_tier`, `one_article_per_order`, `unknown_content_tier`, `content_not_expected`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: '`insufficient_credits`, with unit_price_credits and content_price_credits.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: '`insufficient_scope`, `customer_key_required`, or a ledger refusal: `per_order_cap_exceeded`, `daily_cap_exceeded`, `customer_inactive`, `customer_not_found`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`offer_not_found`, `quote_not_found`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`offer_delisted`, `offer_not_priceable`, `quote_expired` (with expires_at), `offer_not_in_quote`, `price_stale` (with observed_at).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: '`content_invalid`: the inline article would not pass the publisher; `validation` lists the problems. Nothing was charged.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/Unavailable'
    get:
      tags:
      - Orders
      operationId: list_orders
      summary: Your orders
      description: Newest first, with delivery state derived at read time, our latest link audit, and a compact observed outcome. `since` matches orders placed OR delivered at or after it (inclusive); a cancellation of an older order is not surfaced by it — re-read pending orders.
      parameters:
      - name: status
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/OrderStatus'
        description: One order status.
      - name: since
        in: query
        required: false
        schema:
          type: string
          format: date-time
        description: ISO-8601 timestamp.
      - name: target_url
        in: query
        required: false
        schema:
          type: string
          format: uri
        description: Exact URL the links point at.
      - name: target_domain
        in: query
        required: false
        schema:
          type: string
        description: Any order pointing anywhere on this domain.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
        description: Rows.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                  filters:
                    type: object
                    description: The filters as applied.
                  orders:
                    type: array
                    items:
                      $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/orders/{order_id}/cancel:
    post:
      tags:
      - Orders
      operationId: cancel_order
      summary: Cancel before it reaches a supplier
      description: Pulls an order back and refunds it in full, in the same statement. Orders are batched to suppliers each morning, so this is the window in which a mistake is cheaply reversible. Requires the `order` scope.
      parameters:
      - name: order_id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  order_id:
                    type: string
                  status:
                    type: string
                    const: cancelled
                  credits_returned:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: '`not_found`: no cancellable order with that id (including another customer''s).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`already_submitted`: the order has reached the supplier, so it can no longer be cancelled before fulfilment. Nothing is filed with a supplier automatically; a customer can ask us to pursue a supplier recovery review at hello@placementapi.com, and credits are then returned once we have recovered from the supplier and their refund window plus the account grace period have run, or earlier as a goodwill decision.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/orders/{order_id}/outcome:
    get:
      tags:
      - Orders
      operationId: order_outcome
      summary: Everything observed since one placement
      description: Per-keyword rank series across checkpoints, SERP churn, visibility before/after, survival checks, and a confidence with a note. Recomputed on read when the stored row is over an hour old. `outcome` is null with a note until tracking has started.
      parameters:
      - name: order_id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  order_id:
                    type: string
                  status:
                    $ref: '#/components/schemas/OrderStatus'
                  outcome:
                    $ref: '#/components/schemas/Outcome'
                  note:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: '`order_not_found`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/Unavailable'
components:
  schemas:
    Outcome:
      type:
      - object
      - 'null'
      description: 'Everything observed since a placement. Checkpoints: baseline_order, baseline_delivery, d7, d30, d60, d90, d180, d365. Observational: a rank that moved is a fact; that this link moved it is not established. Never say ''caused''.'
      properties:
        order_id:
          type: string
        target_url:
          type: string
        site_id:
          type:
          - string
          - 'null'
        delivered_at:
          type:
          - string
          - 'null'
        days_observed:
          type:
          - integer
          - 'null'
        latest_checkpoint:
          type:
          - string
          - 'null'
        keywords:
          type: array
          items:
            $ref: '#/components/schemas/OutcomeKeyword'
        keywords_tracked:
          type: integer
        keywords_improved:
          type: integer
        keywords_declined:
          type: integer
        entered_top10:
          type: integer
        median_delta:
          type:
          - number
          - 'null'
        volatility_proxy:
          type:
          - number
          - 'null'
        serp_churn:
          type:
          - number
          - 'null'
          description: Share of the baseline top-10 domains no longer in the latest top 10.
        visibility:
          type: object
          properties:
            baseline_traffic:
              type:
              - number
              - 'null'
            latest_traffic:
              type:
              - number
              - 'null'
            baseline_keywords:
              type:
              - integer
              - 'null'
            latest_keywords:
              type:
              - integer
              - 'null'
        survival:
          type: object
          properties:
            live:
              type:
              - boolean
              - 'null'
            checks:
              type: integer
            last_checked:
              type:
              - string
              - 'null'
            first_missing_at:
              type:
              - string
              - 'null'
        confidence:
          type: string
          enum:
          - low
          - medium
          - high
          description: 'About the window, not the size of the movement: under 30 days is noise; one keyword is an anecdote; churn ≥50% says the SERP moved.'
        confidence_note:
          type: string
        computed_at:
          type: string
          format: date-time
    PlacementType:
      type: string
      enum:
      - guest_post
      - niche_edit
      - link_insert
      - homepage_link
      - media_placement
    AnchorPlan:
      type: object
      description: 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.
      required:
      - anchors
      properties:
        anchors:
          type: array
          minItems: 1
          items:
            type: object
            required:
            - text
            - type
            properties:
              text:
                type: string
                minLength: 1
                maxLength: 300
              type:
                type: string
                enum:
                - branded
                - naked_url
                - generic
                - topical
                - partial_match
                - exact_match
              weight:
                type: number
                exclusiveMinimum: 0
                default: 1
                description: Relative weight; normalised across the plan.
    ContentSpec:
      type:
      - object
      - 'null'
      description: The brief to write against when the buyer supplies copy. Null when copy cannot be supplied.
      properties:
        word_count_min:
          type:
          - integer
          - 'null'
        word_count_max:
          type:
          - integer
          - 'null'
        links_allowed:
          type: integer
        anchor_types_forbidden:
          type: array
          items:
            type: string
        rules:
          type: array
          items:
            type: string
          description: Editorial rules in the publisher's own words.
    ContentValidation:
      type: object
      description: Will this pass the publisher? Word count against the spec, external links against the allowance, the target linked exactly once with the anchor the order names, no forbidden anchor type.
      properties:
        ok:
          type: boolean
        word_count:
          type:
          - integer
          - 'null'
        links_found:
          type: integer
        target_link_found:
          type: boolean
        problems:
          type: array
          items:
            type: string
        warnings:
          type: array
          items:
            type: string
    ContentBody:
      type: object
      description: An article. Send body_markdown, body_html or doc_url. A doc_url alone cannot be validated and is accepted with a warning.
      properties:
        title:
          type:
          - string
          - 'null'
          maxLength: 300
        body_markdown:
          type:
          - string
          - 'null'
          maxLength: 200000
        body_html:
          type:
          - string
          - 'null'
          maxLength: 400000
        doc_url:
          type:
          - string
          - 'null'
          format: uri
          maxLength: 2000
        meta_description:
          type:
          - string
          - 'null'
          maxLength: 400
    InventoryMode:
      type: string
      enum:
      - named
      - pre_approval
      - blind
      description: The most important field. 'named' = a specific, identified domain you can inspect before ordering. 'pre_approval' = the vendor proposes candidate domains and the buyer approves them BEFORE placement. 'blind' = a promise (e.g. DR50+, finance) where the domain is assigned at fulfilment with no veto. Only 'named' carries a site_id. Never present a pre_approval or blind offer as though a specific site were secured, and never price-compare a specification against a named domain as the same product.
    OutcomeKeyword:
      type: object
      properties:
        keyword:
          type: string
        baseline:
          type:
          - integer
          - 'null'
        latest:
          type:
          - integer
          - 'null'
        latest_checkpoint:
          type:
          - string
          - 'null'
        delta:
          type:
          - integer
          - 'null'
          description: 'baseline − latest: positive means it moved up.'
        best:
          type:
          - integer
          - 'null'
        series:
          type: array
          items:
            type: object
            properties:
              checkpoint:
                type: string
              rank:
                type:
                - integer
                - 'null'
              observed_at:
                type: string
    OrderContent:
      type: object
      description: 'The article on an order: who writes it, what it cost, when it is due.'
      properties:
        mode:
          type: string
          enum:
          - publisher
          - service
          - supplied
          - papi_written
        tier:
          type:
          - string
          - 'null'
        price_credits:
          type: integer
        due_at:
          type:
          - string
          - 'null'
          format: date-time
        if_no_content:
          type:
          - string
          - 'null'
          enum:
          - convert_to_service
          - cancel
          - null
        version:
          type: integer
          description: How many versions have been sent so far.
        awaiting:
          type: boolean
          description: 'True in awaiting_content or content_revision_requested: the order is paid and held, not with a supplier.'
    OrderStatus:
      type: string
      enum:
      - ordered
      - awaiting_content
      - content_revision_requested
      - awaiting_operator
      - submitted_to_vendor
      - delivered
      - cancelled
      - refunded
      - failed
    Order:
      type: object
      description: An order as listed by GET /v1/orders. Supplier identity is intentionally not exposed anywhere in this API. You are buying from us; which upstream marketplace fulfils a placement is not something you need or can act on.
      properties:
        order_id:
          type: string
        status:
          $ref: '#/components/schemas/OrderStatus'
        offer_id:
          type: string
        placement_type:
          $ref: '#/components/schemas/PlacementType'
        inventory_mode:
          $ref: '#/components/schemas/InventoryMode'
        site_id:
          type:
          - string
          - 'null'
        target_url:
          type: string
        anchor_text:
          type:
          - string
          - 'null'
        anchor_plan:
          oneOf:
          - $ref: '#/components/schemas/AnchorPlan'
          - type: 'null'
        target_keyword:
          type:
          - string
          - 'null'
        quantity:
          type: integer
        total_credits:
          type: integer
          description: Credits. 1 credit = 1 US cent.
        ordered_at:
          type: string
          format: date-time
        due_at:
          type:
          - string
          - 'null'
          format: date-time
        content:
          $ref: '#/components/schemas/OrderContent'
        cancellable:
          type: boolean
          description: True while nothing has been sent to a supplier yet.
        submitted_at:
          type:
          - string
          - 'null'
          format: date-time
        delivered_at:
          type:
          - string
          - 'null'
          format: date-time
        placement_url:
          type:
          - string
          - 'null'
        delivery_state:
          type: string
          enum:
          - in_progress
          - on_time
          - delivered_late
          - overdue
          - refund_eligible
          - unknown_turnaround
          - closed
          description: Derived at read time, never stored. `refund_eligible` is a delivery-timing milestone (past due plus the account grace period) judged from the calendar alone; it does not check supplier recovery, does not mean a claim has been filed with a supplier, and is not a refund decision.
        days_late:
          type:
          - integer
          - 'null'
        refund_eligible_at:
          type:
          - string
          - 'null'
          format: date-time
          description: 'When the order passes due_at plus the account grace period. Delivery timing only: it does not mean credits have been returned and it is not an automatic credit refund. A credit refund for an order already with a supplier requires that we have recovered from the supplier, plus the supplier''s refund window and the account grace period. The customer can ask us to pursue a supplier recovery review at hello@placementapi.com.'
        supplier_report:
          type:
          - string
          - 'null'
          description: 'The supplier''s latest word from the check-in loop: completed | on_time | delayed, or null.'
        supplier_report_at:
          type:
          - string
          - 'null'
        link_audit:
          type:
          - object
          - 'null'
          description: 'Our own audit of the delivered page: link present in served HTML, dofollow, bought anchor intact. Null before the first sweep.'
          properties:
            checked_at:
              type: string
            link_found:
              type: boolean
            dofollow:
              type:
              - boolean
              - 'null'
            anchor_matched:
              type:
              - boolean
              - 'null'
        turnaround_revised:
          type: boolean
        outcome:
          type:
          - object
          - 'null'
          description: Compact outcome (no per-keyword series). `detail` names GET /v1/orders/{order_id}/outcome. Null until tracking has produced anything.
          properties:
            days_observed:
              type:
              - integer
              - 'null'
            latest_checkpoint:
              type:
              - string
              - 'null'
            keywords:
              type: array
              items:
                type: object
            keywords_improved:
              type: integer
            keywords_declined:
              type: integer
            entered_top10:
              type: integer
            median_delta:
              type:
              - number
              - 'null'
            serp_churn:
              type:
              - number
              - 'null'
            visibility:
              type: object
            survival:
              type: object
            confidence:
              type: string
              enum:
              - low
              - medium
              - high
            confidence_note:
              type: string
            detail:
              type: string
    Error:
      type: object
      required:
      - error
      properties:
        error:
          type: string
          description: Machine-readable code, snake_case.
        detail:
          description: Human-readable explanation, or a zod flatten() object on validation failures.
      additionalProperties: true
  responses:
    Unavailable:
      description: '`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.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: '`insufficient_scope` when the key lacks the `order` scope; `customer_key_required` when the operator token is used where a customer must be attributable.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    BadRequest:
      description: Malformed request. `error` is `bad_request` (with a zod `detail`), `invalid_json`, or `unknown_parameter` / `unknown_field` (with `unknown[]` and `accepted[]`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: 'Missing, malformed or revoked Bearer credential: `{ error: "unauthorized" }`.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
      description: 'Required. A retry without one is indistinguishable from a second purchase and is refused (400 idempotency_key_required) rather than double-charged. A replay with the same key returns the original result with `replayed: true`.'
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: 'Customer API key (`pk_live_…`) in `Authorization: Bearer <key>`. Issue from POST /v1/keys or the dashboard. Endpoints marked session-only reject API keys.'