placementapi-com Checkout API

Card checkout for a basket, with or without an account.

Operations 2

POST /v1/checkout/basket Buy a basket by card, with or without an account #
GET /v1/checkout/basket/{basket_id} How a checkout went #

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-checkout-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-checkout-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Placement Checkout 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: Checkout
  description: Card checkout for a basket, with or without an account.
paths:
  /v1/checkout/basket:
    post:
      tags:
      - Checkout
      operationId: checkout_basket
      summary: Buy a basket by card, with or without an account
      description: 'Auth optional. Without a key: `email` is required, payment creates the account, and the buyer receives a sign-in link with their orders placed. With a customer key: the balance is spent first and only the shortfall is charged; a basket the balance covers is placed immediately with no card (checkout_url null), and an Idempotency-Key header is REQUIRED. A card processing fee (2.9% + 30c, grossed up) is a separate line item. Nothing is charged or ordered by this call itself; orders are placed by the payment webhook once money is captured. A presented-but-invalid key is a 401, not a guest. An optional buyer-approved `max_total_credits` ceiling refuses the checkout with 409 `price_changed` if the reviewed total has risen, so a catalogue move between review and buy is never charged silently; a purchase is only ever placed on an explicit buyer action, never by the comparison itself. Limited to 10 baskets an hour per address and per customer.'
      security:
      - {}
      - apiKey: []
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        schema:
          type: string
        description: Required when authenticated; optional for guests (the card form is the confirmation).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - return_url
              - lines
              properties:
                email:
                  type: string
                  format: email
                  description: Required when unauthenticated; ignored when a key is sent.
                return_url:
                  type: string
                  format: uri
                  description: https URL on an allowed host; where the buyer is sent back with ?basket=&token= appended.
                max_total_credits:
                  type: integer
                  minimum: 1
                  description: Optional, additive buyer-approved ceiling on the whole basket in credits (placement + content). When the audited total has risen above it, the checkout is refused with 409 `price_changed` carrying `current_total_credits` and `max_total_credits`, and nothing is debited, charged or ordered; the buyer must compare again and accept the current total. Absent, the route behaves exactly as before (`repriced` tolerance unchanged). This is the ceiling the hosted Perch review sends as the total it showed the buyer.
                lines:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required:
                    - offer_id
                    - target_url
                    properties:
                      offer_id:
                        type: string
                      quote_id:
                        type: string
                      target_url:
                        type: string
                        format: uri
                      anchor_text:
                        type:
                        - string
                        - 'null'
                        maxLength: 300
                      target_keyword:
                        type:
                        - string
                        - 'null'
                        maxLength: 200
                      content_mode:
                        type: string
                        enum:
                        - publisher
                        - service
                        - supplied
                        - papi_written
                      content_tier:
                        type:
                        - string
                        - 'null'
                        maxLength: 60
                        description: A paid writing tier by name when content_mode is service; the cheapest when omitted.
                      if_no_content:
                        type: string
                        enum:
                        - convert_to_service
                        - cancel
      responses:
        '201':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  basket_id:
                    type: string
                  status_token:
                    type: string
                  checkout_url:
                    type:
                    - string
                    - 'null'
                    description: Null when the balance covered everything.
                  status:
                    type: string
                  total_credits:
                    type: integer
                    description: placement_credits + content_credits.
                  placement_credits:
                    type: integer
                  content_credits:
                    type: integer
                    description: Writing fees priced into the basket (service tier or papi_written).
                  fee_cents:
                    type: integer
                  charge_cents:
                    type: integer
                  balance_used_credits:
                    type: integer
                  shortfall_credits:
                    type: integer
                  expires_at:
                    type: string
                    format: date-time
                  replayed:
                    type: boolean
                  lines:
                    type: array
                    items:
                      type: object
        '400':
          description: '`bad_request`, `return_url_not_allowed`, `email_required`, `idempotency_key_required`, `empty_basket`, `anchor_text_required` (with offer_id).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`customer_key_required`: the operator token cannot buy.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`repriced` with lines[{ offer_id, was_credits, now_credits }] when a held price moved more than 10%; `price_changed` (with `current_total_credits` and `max_total_credits`) when the basket now costs more than the buyer-approved `max_total_credits` ceiling, in which case nothing is debited or ordered and the buyer must compare again; `basket_already_resolved`; or a line problem such as `offer_not_found` naming the offer.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/checkout/basket/{basket_id}:
    get:
      tags:
      - Checkout
      operationId: checkout_basket_status
      security: []
      summary: How a checkout went
      description: Public and token-gated, because the buyer may have no account yet. Poll after returning from the card form. A wrong token is a 404, not a 403. On `partial`, credits for unplaced lines stay on the buyer's balance. Says nothing else about the customer.
      parameters:
      - name: basket_id
        in: path
        required: true
        schema:
          type: string
      - name: token
        in: query
        required: true
        schema:
          type: string
        description: status_token from the checkout response.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    description: pending | paid | placed | partial | failed | abandoned.
                  total_credits:
                    type: integer
                  fee_cents:
                    type: integer
                  charge_cents:
                    type: integer
                  balance_used_credits:
                    type: integer
                  lines:
                    type: array
                    items:
                      type: object
                      properties:
                        offer_id:
                          type: string
                        domain:
                          type:
                          - string
                          - 'null'
                        price_credits:
                          type: integer
                        order_id:
                          type:
                          - string
                          - 'null'
                        cancellable:
                          type: boolean
                        error:
                          type:
                          - string
                          - 'null'
                  dashboard_url:
                    type: string
                  email_hint:
                    type:
                    - string
                    - 'null'
                    description: Masked.
        '404':
          description: '`not_found`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/Unavailable'
components:
  responses:
    RateLimited:
      description: Rate limited. 1,000 requests and 20,000 returned rows per key per hour, a monthly row entitlement that grows with credits purchased, and a maximum offset of 10,000. Carries `Retry-After` and `X-RateLimit-*` / `X-Quota-Rows-*` headers.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    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'
    Unauthorized:
      description: 'Missing, malformed or revoked Bearer credential: `{ error: "unauthorized" }`.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    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
  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.'