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

# Create an offer

> Prices one product for one named buyer and returns a private checkout link made out to
them. The price, the quantity and the buyer are settled here and are read-only on the
offer page, so everything the order will need has to be supplied in this request. The
offer accepts the payment methods the product already accepts; they cannot be chosen per
offer. Creating an offer does not notify the buyer — the link comes back as `public_url`
and is yours to send.

### Choosing a payment plan

`pricing.plan_type` decides the rest of the body. Send the fields its row asks for;
every other field under `pricing` is optional under all three plan types.

| `plan_type` | What the buyer is charged | Send alongside `plan_type` and `price_minor_units` |
| --- | --- | --- |
| `one_time` | `price_minor_units`, once. | nothing |
| `subscription` | `price_minor_units` now, then `recurring_amount_minor_units` every billing period until the subscription is cancelled. | `recurring_amount_minor_units`, `interval`, `interval_count` |
| `installment` | `price_minor_units` now, then `recurring_amount_minor_units` every billing period until `installment_count` payments have been made in total, counting the first. | `recurring_amount_minor_units`, `interval`, `interval_count`, `installment_count` |

The billing period is `interval` × `interval_count` — `month` × 3 bills quarterly — and a
single period may not be longer than two years. `second_payment_in`, `trial_days`,
`shipping_price_minor_units` and `custom_plan_name` stay optional under every plan type.
`installment` is accepted only from sellers enabled for installment sales; `one_time` and
`subscription` are open to every seller.

### Opening the offer inside your own page

By default the buyer follows `public_url`, which redirects them to the offer's checkout on
COPE. Send `embed_origin` instead and the same checkout can be framed in your own page:
the offer comes back with an `embed_url` and `embed_origin` echoing the one origin allowed
to frame it. Both are absent — not null — on an offer that cannot be framed, and
`public_url` keeps working either way.

Hand both to the Checkout SDK's `mountCheckout` rather
than building the iframe yourself: the framed page waits for a mount handshake from its
parent before it starts, so a bare `<iframe src=embed_url>` never loads.

`embed_origin` must be an origin you have already registered and enabled for checkout
embedding. An origin you have not registered, one another seller registered, and one that
has been disabled or archived are each refused with 422 `embed_origin_not_allowed`, and no
offer is created. Disabling or archiving the origin afterwards stops every offer embed
bound to it.



## OpenAPI

````yaml /api-reference/commerce-v1.openapi.json post /v1/commerce/offers
openapi: 3.0.3
info:
  description: >-
    Public REST API for COPE vendor integrations. Authenticate with a COPE API
    key or Clerk bearer token and call the endpoints described below.
  title: COPE Public API
  version: v1
servers:
  - description: Production
    url: https://api.cope.com
security:
  - cope_sk: []
paths:
  /v1/commerce/offers:
    post:
      tags:
        - Offers
      summary: Create an offer
      description: >-
        Prices one product for one named buyer and returns a private checkout
        link made out to

        them. The price, the quantity and the buyer are settled here and are
        read-only on the

        offer page, so everything the order will need has to be supplied in this
        request. The

        offer accepts the payment methods the product already accepts; they
        cannot be chosen per

        offer. Creating an offer does not notify the buyer — the link comes back
        as `public_url`

        and is yours to send.


        ### Choosing a payment plan


        `pricing.plan_type` decides the rest of the body. Send the fields its
        row asks for;

        every other field under `pricing` is optional under all three plan
        types.


        | `plan_type` | What the buyer is charged | Send alongside `plan_type`
        and `price_minor_units` |

        | --- | --- | --- |

        | `one_time` | `price_minor_units`, once. | nothing |

        | `subscription` | `price_minor_units` now, then
        `recurring_amount_minor_units` every billing period until the
        subscription is cancelled. | `recurring_amount_minor_units`, `interval`,
        `interval_count` |

        | `installment` | `price_minor_units` now, then
        `recurring_amount_minor_units` every billing period until
        `installment_count` payments have been made in total, counting the
        first. | `recurring_amount_minor_units`, `interval`, `interval_count`,
        `installment_count` |


        The billing period is `interval` × `interval_count` — `month` × 3 bills
        quarterly — and a

        single period may not be longer than two years. `second_payment_in`,
        `trial_days`,

        `shipping_price_minor_units` and `custom_plan_name` stay optional under
        every plan type.

        `installment` is accepted only from sellers enabled for installment
        sales; `one_time` and

        `subscription` are open to every seller.


        ### Opening the offer inside your own page


        By default the buyer follows `public_url`, which redirects them to the
        offer's checkout on

        COPE. Send `embed_origin` instead and the same checkout can be framed in
        your own page:

        the offer comes back with an `embed_url` and `embed_origin` echoing the
        one origin allowed

        to frame it. Both are absent — not null — on an offer that cannot be
        framed, and

        `public_url` keeps working either way.


        Hand both to the Checkout SDK's `mountCheckout` rather

        than building the iframe yourself: the framed page waits for a mount
        handshake from its

        parent before it starts, so a bare `<iframe src=embed_url>` never loads.


        `embed_origin` must be an origin you have already registered and enabled
        for checkout

        embedding. An origin you have not registered, one another seller
        registered, and one that

        has been disabled or archived are each refused with 422
        `embed_origin_not_allowed`, and no

        offer is created. Disabling or archiving the origin afterwards stops
        every offer embed

        bound to it.
      operationId: createCommerceOffer
      parameters:
        - description: >-
            Required. At most 255 characters of valid UTF-8 with no NUL byte. A
            retry that carries the same key and the same body returns the
            original response instead of performing the write a second time. The
            same key with a different body is refused with 409
            idempotency_conflict. Keys are remembered for 24 hours.
          in: header
          name: Idempotency-Key
          required: true
          schema:
            maxLength: 255
            type: string
      requestBody:
        content:
          application/json:
            examples:
              one_time:
                summary: One-time payment — the buyer is charged 499.00 EUR once
                value:
                  buyer:
                    address:
                      city: Berlin
                      line1: Hauptstrasse 12
                      postal_code: '10115'
                    country: DE
                    email: ada.fischer@example.com
                    first_name: Ada
                    last_name: Fischer
                    type: person
                  currency: EUR
                  pricing:
                    plan_type: one_time
                    price_minor_units: 49900
                  product_id: prod_A1b2C3d4
                  quantity: 1
              subscription:
                summary: >-
                  Subscription — 49.00 EUR now and every month until it is
                  cancelled
                value:
                  buyer:
                    address:
                      city: Berlin
                      line1: Hauptstrasse 12
                      postal_code: '10115'
                    country: DE
                    email: ada.fischer@example.com
                    first_name: Ada
                    last_name: Fischer
                    type: person
                  currency: EUR
                  pricing:
                    interval: month
                    interval_count: 1
                    plan_type: subscription
                    price_minor_units: 4900
                    recurring_amount_minor_units: 4900
                  product_id: prod_A1b2C3d4
                  quantity: 1
              installment:
                summary: >-
                  Installment plan — 499.00 EUR as four monthly payments of
                  124.75 EUR
                value:
                  buyer:
                    address:
                      city: Berlin
                      line1: Hauptstrasse 12
                      postal_code: '10115'
                    country: DE
                    email: ada.fischer@example.com
                    first_name: Ada
                    last_name: Fischer
                    type: person
                  currency: EUR
                  pricing:
                    installment_count: 4
                    interval: month
                    interval_count: 1
                    plan_type: installment
                    price_minor_units: 12475
                    recurring_amount_minor_units: 12475
                  product_id: prod_A1b2C3d4
                  quantity: 1
            schema:
              properties:
                buyer:
                  properties:
                    address:
                      properties:
                        city:
                          description: Required wherever line1 is.
                          minLength: 1
                          type: string
                        line1:
                          description: >-
                            Required for a product that ships, and for a digital
                            product or service sold to a buyer in the EU or the
                            UK, where the invoice must carry a complete address.
                          minLength: 1
                          type: string
                        line2:
                          minLength: 1
                          type: string
                        postal_code:
                          description: >-
                            Required for every country, with no exception: the
                            tax quote is derived from it, and the buyer cannot
                            add one later on the offer page.
                          minLength: 1
                          type: string
                      required:
                        - postal_code
                      type: object
                    company_name:
                      description: Required when the buyer is a company.
                      minLength: 1
                      type: string
                    country:
                      description: Buyer's country, as a two-letter ISO 3166-1 code.
                      pattern: ^[A-Z]{2}$
                      type: string
                    email:
                      description: >-
                        Where the offer link is sent when you send it, and the
                        address the receipt goes to.
                      format: email
                      type: string
                    first_name:
                      minLength: 1
                      type: string
                    last_name:
                      minLength: 1
                      type: string
                    locale:
                      description: >-
                        Language of the offer page and the checkout. Defaults to
                        `en`.
                      minLength: 1
                      type: string
                    phone:
                      description: >-
                        Optional, and must be a valid phone number when it is
                        given.
                      minLength: 1
                      type: string
                    state:
                      description: >-
                        Required for buyers in US, CA, AU — tax there is
                        calculated per state.
                      minLength: 1
                      type: string
                    type:
                      enum:
                        - person
                        - company
                      type: string
                    vat_id:
                      minLength: 1
                      type: string
                  required:
                    - type
                    - email
                    - first_name
                    - last_name
                    - country
                    - address
                  type: object
                currency:
                  description: >-
                    Currency of the offer, as an ISO 4217 code. It must be one
                    the seller can be paid in — anything else is refused with a
                    422 naming the ones they can.
                  pattern: ^[A-Z]{3}$
                  type: string
                embed_origin:
                  description: >-
                    Origin allowed to frame this offer's checkout, so the buyer
                    can pay inside your own page instead of following the
                    redirect. It must be one you have already registered and
                    enabled for checkout embedding; anything else — including an
                    origin another seller registered — is refused with 422
                    `embed_origin_not_allowed` and no offer is created. Omit it
                    for a redirect-only offer.
                  pattern: ^https://[^@/?#\s]+$
                  type: string
                link_expiration:
                  description: Omit the object entirely to issue a link that never expires.
                  properties:
                    duration:
                      description: >-
                        How many units the link stays valid for. Required unless
                        the mode is never.
                      minimum: 1
                      type: integer
                    mode:
                      enum:
                        - never
                        - custom
                      type: string
                    unit:
                      description: Required unless the mode is never.
                      enum:
                        - minutes
                        - hours
                        - days
                      type: string
                  required:
                    - mode
                  type: object
                pricing:
                  description: >-
                    Shape and amounts of the payment. `plan_type` decides which
                    of the fields below you have to send: `one_time` needs
                    nothing beyond `price_minor_units`; `subscription` also
                    needs `recurring_amount_minor_units`, `interval` and
                    `interval_count`; `installment` needs those three and
                    `installment_count` as well. Every other field here is
                    optional under all three.
                  properties:
                    custom_plan_name:
                      description: >-
                        Name shown for the plan on the offer page and at
                        checkout. Defaults to a label read from the plan type.
                      minLength: 1
                      type: string
                    installment_count:
                      description: >-
                        Total number of payments, counting the first. Required
                        for installment.
                      minimum: 1
                      type: integer
                    interval:
                      description: >-
                        Unit of the billing period. Required for subscription
                        and installment.
                      enum:
                        - day
                        - week
                        - month
                        - year
                      type: string
                    interval_count:
                      description: >-
                        How many intervals pass between payments. Required for
                        subscription and installment; one billing period may not
                        be longer than 2 years.
                      minimum: 1
                      type: integer
                    plan_type:
                      description: >-
                        Shape of the payment. A value the seller is not enabled
                        for is refused with a 422 naming the ones they are —
                        installment sales are enabled per seller.
                      enum:
                        - one_time
                        - subscription
                        - installment
                      type: string
                    price_minor_units:
                      description: >-
                        First payment, in the minor units of the offer currency.
                        It must sit within the charge bounds for that currency.
                      maximum: 99999999
                      minimum: 200
                      type: integer
                    recurring_amount_minor_units:
                      description: >-
                        Amount of every payment after the first, in the minor
                        units of the offer currency. Required for subscription
                        and installment.
                      maximum: 99999999
                      minimum: 200
                      type: integer
                    second_payment_in:
                      description: >-
                        Days between the first payment and the second. Omit it
                        to charge the second payment one billing period after
                        the first.
                      minimum: 1
                      type: integer
                    shipping_price_minor_units:
                      description: >-
                        Flat shipping fee for the order, in the minor units of
                        the offer currency. It is charged only when the product
                        ships — a digital product carries no shipping whatever
                        is sent here.
                      minimum: 0
                      type: integer
                    trial_days:
                      description: Free days before the first charge, at most 30.
                      maximum: 30
                      minimum: 0
                      type: integer
                  required:
                    - plan_type
                    - price_minor_units
                  type: object
                product_id:
                  description: >-
                    Product the offer sells. It must belong to the seller the
                    token is issued for; any other product answers 404.
                  example: prod_A1b2C3d4
                  pattern: ^prod_[A-Za-z0-9]{8}$
                  type: string
                quantity:
                  default: 1
                  description: >-
                    Units of the product on the offer. Defaults to 1, and may
                    not exceed 100.
                  maximum: 100
                  minimum: 1
                  type: integer
              required:
                - product_id
                - currency
                - pricing
                - buyer
              type: object
      responses:
        '201':
          content:
            application/json:
              schema:
                additionalProperties: false
                properties:
                  data:
                    additionalProperties: false
                    properties:
                      buyer_profile:
                        additionalProperties: false
                        properties:
                          address:
                            additionalProperties: false
                            properties:
                              city:
                                type: string
                              line1:
                                type: string
                              line2:
                                nullable: true
                                type: string
                              postal_code:
                                type: string
                            required:
                              - line1
                              - line2
                              - city
                              - postal_code
                            type: object
                          company_name:
                            nullable: true
                            type: string
                          country:
                            nullable: true
                            type: string
                          email:
                            type: string
                          first_name:
                            nullable: true
                            type: string
                          last_name:
                            nullable: true
                            type: string
                          locale:
                            type: string
                          phone:
                            description: >-
                              Buyer's phone number, as captured when the offer
                              was created.
                            nullable: true
                            type: string
                          state:
                            nullable: true
                            type: string
                          type:
                            type: string
                          vat_id:
                            nullable: true
                            type: string
                        required:
                          - type
                          - email
                          - first_name
                          - last_name
                          - country
                          - state
                          - locale
                          - address
                          - company_name
                          - vat_id
                          - phone
                        type: object
                      cancelled_at:
                        nullable: true
                        type: string
                      cancelled_reason:
                        nullable: true
                        type: string
                      created_at:
                        type: string
                      embed_origin:
                        description: >-
                          The one origin allowed to frame `embed_url`. Absent —
                          not null — when the offer cannot be framed.
                        pattern: ^https://[^@/?#\s]+$
                        type: string
                      embed_url:
                        description: >-
                          Pass to the Checkout SDK's `mountCheckout` on
                          `embed_origin` to open this offer's checkout inside
                          your own page. The framed page waits for the SDK's
                          mount handshake, so a bare iframe never loads. The
                          same checkout `public_url` redirects to, not a second
                          one. Absent — not null — when the offer cannot be
                          framed.
                        type: string
                      expires_at:
                        description: >-
                          When the offer link stops working. Null when the offer
                          never expires, which is what an offer created without
                          `link_expiration` gets.
                        nullable: true
                        type: string
                      id:
                        example: of_A1b2C3d4E5f6
                        pattern: ^of_[A-Za-z0-9]{12}$
                        type: string
                      last_buyer_notification_at:
                        nullable: true
                        type: string
                      next_resend_available_at:
                        nullable: true
                        type: string
                      order:
                        additionalProperties: false
                        nullable: true
                        properties:
                          id:
                            example: ord_A1b2C3d4E5f6
                            pattern: ^ord_[A-Za-z0-9]{12}$
                            type: string
                        required:
                          - id
                        type: object
                      ordered_at:
                        nullable: true
                        type: string
                      payment_plan_snapshot:
                        additionalProperties: false
                        properties:
                          currency:
                            type: string
                          custom_plan_name:
                            nullable: true
                            type: string
                          installment_count:
                            nullable: true
                            type: integer
                          interval:
                            nullable: true
                            type: string
                          interval_count:
                            nullable: true
                            type: integer
                          plan_type:
                            type: string
                          price_minor_units:
                            type: integer
                          quantity:
                            type: integer
                          recurring_amount_minor_units:
                            nullable: true
                            type: integer
                          second_payment_in:
                            nullable: true
                            type: integer
                          shipping_price_minor_units:
                            nullable: true
                            type: integer
                          trial_days:
                            nullable: true
                            type: integer
                        required:
                          - plan_type
                          - currency
                          - quantity
                          - price_minor_units
                          - shipping_price_minor_units
                          - recurring_amount_minor_units
                          - interval
                          - interval_count
                          - installment_count
                          - second_payment_in
                          - trial_days
                          - custom_plan_name
                        type: object
                      product:
                        additionalProperties: false
                        nullable: true
                        properties:
                          id:
                            example: prod_A1b2C3d4
                            pattern: ^prod_[A-Za-z0-9]{8}$
                            type: string
                          name:
                            type: string
                        required:
                          - id
                          - name
                        type: object
                      public_url:
                        type: string
                      regeneration_eligible:
                        type: boolean
                      status:
                        type: string
                    required:
                      - buyer_profile
                      - cancelled_at
                      - cancelled_reason
                      - created_at
                      - expires_at
                      - id
                      - last_buyer_notification_at
                      - next_resend_available_at
                      - order
                      - ordered_at
                      - payment_plan_snapshot
                      - product
                      - public_url
                      - regeneration_eligible
                      - status
                    type: object
                required:
                  - data
                type: object
          description: Successful response
        '400':
          content:
            application/problem+json:
              examples:
                idempotency_key_required:
                  summary: Missing Idempotency-Key header
                  value:
                    code: idempotency_key_required
                    detail: Idempotency-Key header is required for this operation
                    errors:
                      - code: idempotency_key_required
                        detail: Idempotency-Key header is required for this operation
                    request_id: req_123
                    status: 400
                    title: Idempotency Key Required
                    type: https://docs.cope.com/errors/idempotency_key_required
                unparsable_request:
                  summary: Unparsable request
                  value:
                    code: invalid_request
                    detail: >-
                      The request could not be parsed. Check the query string
                      and the request body.
                    request_id: req_123
                    status: 400
                    title: Invalid Request
                    type: https://docs.cope.com/errors/invalid_request
                invalid_idempotency_key:
                  summary: Malformed Idempotency-Key header
                  value:
                    code: invalid_idempotency_key
                    detail: >-
                      Idempotency-Key header must be at most 255 characters of
                      valid UTF-8 without a NUL byte
                    request_id: req_123
                    status: 400
                    title: Invalid Idempotency Key
                    type: https://docs.cope.com/errors/invalid_idempotency_key
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: Invalid request
        '401':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: Missing or invalid bearer token
        '403':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: Bearer token is not authorized for this route
        '404':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: Resource not found
        '409':
          content:
            application/problem+json:
              examples:
                idempotency_conflict_in_flight:
                  summary: A request with this key is still processing
                  value:
                    code: idempotency_conflict
                    detail: >-
                      A request with this idempotency key is currently being
                      processed
                    request_id: req_123
                    status: 409
                    title: Idempotency Conflict
                    type: https://docs.cope.com/errors/idempotency_conflict
                idempotency_conflict_body_mismatch:
                  summary: The key was reused with a different body
                  value:
                    code: idempotency_conflict
                    detail: >-
                      Request body does not match the original request for this
                      idempotency key
                    request_id: req_123
                    status: 409
                    title: Idempotency Conflict
                    type: https://docs.cope.com/errors/idempotency_conflict
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: >-
            The idempotency key cannot be honoured: either the first request
            carrying it is still processing, or it was reused with a different
            body. Retry the first case unchanged to collect the original
            response; the second needs the original body, or a new key for a
            genuinely new request.
        '422':
          content:
            application/problem+json:
              examples:
                offer_validation_failed:
                  summary: Validation failed
                  value:
                    code: validation_failed
                    detail: null
                    errors:
                      - code: invalid
                        detail: is required for all countries
                        param: buyer.address.postal_code
                    request_id: req_123
                    status: 422
                    title: Validation Failed
                    type: https://docs.cope.com/errors/validation_failed
                offer_embed_origin_not_allowed:
                  summary: >-
                    embed_origin is not a registered, enabled origin for this
                    seller
                  value:
                    code: embed_origin_not_allowed
                    detail: >-
                      embed_origin is not a registered, enabled embed domain for
                      this seller. Register it first, then create the offer.
                    errors:
                      - code: embed_origin_not_allowed
                        detail: >-
                          embed_origin is not a registered, enabled embed domain
                          for this seller. Register it first, then create the
                          offer.
                    request_id: req_123
                    status: 422
                    title: Embed Origin Not Allowed
                    type: https://docs.cope.com/errors/embed_origin_not_allowed
                tax_address_invalid:
                  summary: Tax address invalid
                  value:
                    code: tax_address_invalid
                    detail: >-
                      We couldn't calculate tax for this address. Please check
                      it and try again.
                    errors:
                      - code: tax_address_invalid
                        detail: >-
                          We couldn't calculate tax for this address. Please
                          check it and try again.
                    request_id: req_123
                    status: 422
                    title: Tax Address Invalid
                    type: https://docs.cope.com/errors/tax_address_invalid
                tax_request_rejected:
                  summary: Tax request rejected
                  value:
                    code: tax_request_rejected
                    detail: >-
                      We couldn't calculate tax for this order. Please review
                      the billing details, or contact support if the problem
                      continues.
                    errors:
                      - code: tax_request_rejected
                        detail: >-
                          We couldn't calculate tax for this order. Please
                          review the billing details, or contact support if the
                          problem continues.
                    request_id: req_123
                    status: 422
                    title: Tax Request Rejected
                    type: https://docs.cope.com/errors/tax_request_rejected
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: Public commerce validation problem
        '503':
          content:
            application/problem+json:
              examples:
                tax_service_unavailable:
                  summary: Tax service unavailable
                  value:
                    code: tax_service_unavailable
                    detail: Tax calculation is temporarily unavailable
                    request_id: req_123
                    status: 503
                    title: Tax Service Unavailable
                    type: https://docs.cope.com/errors/tax_service_unavailable
              schema:
                $ref: '#/components/schemas/PublicProblemDetail'
          description: Tax calculation is temporarily unavailable
      security:
        - cope_sk: []
components:
  schemas:
    PublicProblemDetail:
      additionalProperties: false
      properties:
        code:
          type: string
        detail:
          nullable: true
          type: string
        errors:
          items:
            additionalProperties: false
            properties:
              code:
                type: string
              detail:
                type: string
              param:
                type: string
            required:
              - code
              - detail
            type: object
          type: array
        request_id:
          type: string
        status:
          type: integer
        title:
          type: string
        type:
          type: string
      required:
        - type
        - title
        - status
        - code
        - request_id
      type: object
  securitySchemes:
    cope_sk:
      description: >-
        Bearer credential for the public API. Vendor integrations should send a
        live COPE API key (`cope_sk_live_*`). Clerk bearer tokens are also
        accepted when paired with `X-Cope-Business-Id`.
      scheme: bearer
      type: http

````