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

# Reprice: supersede a price

> Requires `billing:write`. **This is how the money changes.** A price cannot be edited — there is no `PATCH` on one, and the database refuses an in-place edit of the amount, the currency, the kind, the recurrence, the tax posture or the plan.

The reason is that a price is what somebody **agreed** to pay. A subscription refers to the price it was sold at, so editing the amount silently rewrites what an existing customer consented to and leaves no trace it was ever different.

So repricing is: archive the old price, create the new one. Both happen here, in one transaction, in that order — and the order is not interchangeable. Only one live price may exist per plan per currency per recurrence, so creating first would be refused in the ordinary case (the same offer at a new number), and doing the two calls yourself leaves a window where the plan has no live price at all. A failure leaves the old price exactly where it was.

The new price inherits the **plan** and nothing else: state the whole offer, currency included. Existing subscriptions keep the archived price; new customers get the new one.



## OpenAPI

````yaml /api-reference/openapi.json post /v1/organization/catalogue/prices/{id}/reprice
openapi: 3.1.0
info:
  title: UserKit API
  version: 1.0.0
  description: >-
    The HTTP surface of UserKit.


    Two planes share one API. The **staff plane** is what a human uses in the
    panel: users, organizations, roles, members, keys. The **customer plane** is
    what a developer's own product uses: contacts, identities, hosted or
    federated sign-in.


    Every error answers the same envelope — `{"error": {"code", "message"}}`.
    The `code` is a stable contract to branch on; the `message` is for a person
    and may change.
servers:
  - url: '{baseUrl}'
    description: The API host.
    variables:
      baseUrl:
        default: https://api.userkit.dev
        description: Base URL of the API, no trailing slash.
security:
  - sessionToken: []
tags:
  - name: Authentication
    description: >-
      Public sign-up, sign-in, two-factor and password recovery for staff
      accounts.
  - name: Session
    description: >-
      The active session: who the caller is, which organization they are in, and
      signing out.
  - name: Account
    description: The caller's own account — profile, password, sessions, avatar.
  - name: Two-factor
    description: >-
      TOTP setup, activation and recovery codes. Returns 501 when two-factor is
      unavailable on the server.
  - name: Organizations
    description: The organizations a user belongs to, and the active one.
  - name: Environments
    description: >-
      The live and test environments seeded with every organization, and their
      identity settings.
  - name: Members
    description: Memberships and invitations.
  - name: Roles
    description: Roles and the permission catalogue they draw from.
  - name: Audit log
    description: >-
      What staff did inside an organization. Append-only, and read behind its
      own permission.
  - name: API keys
    description: Secret keys (`uk_sk_…`) and publishable keys (`uk_pk_…`).
  - name: Contacts (staff session)
    description: >-
      The staff view of the customer plane, opened by a staff session. Reads
      take `?environment=` as an explicit view parameter.
  - name: Contacts (API key)
    description: >-
      The machine surface, authenticated by an API key. The environment is the
      key's environment and cannot be named by the caller.
  - name: Customer plane
    description: >-
      Called from the developer's own pages with a publishable key: boot, magic
      link, and the contact's own session.
  - name: Hosted auth
    description: >-
      Sign-up, sign-in, verification and recovery for contacts, when UserKit
      owns the account.
  - name: Customer teams
    description: >-
      A customer is a team. Its roster, its invitations and its two roles —
      owner and member — administered by the contact's own session. The active
      customer travels in `X-Customer-Id`.
  - name: Webhooks
    description: >-
      Outbound webhooks: endpoints, the published event catalogue, the delivery
      log, replay and test sends. Never gated by a plan — webhooks are a
      developer primitive.
  - name: Catalogue
    description: >-
      The plans, prices and features **you** sell to your own customers. Per
      environment, a price per currency, and `recurring` or `one_time`. Distinct
      from the plan you are on with UserKit, which is `GET
      /v1/organization/entitlements`.
  - name: Subscriptions
    description: >-
      What one of **your** customers is paying you, mirrored from the gateway
      that charges them. The gateway is the truth about money — it holds the
      schedule, runs the retries and decides what a proration is worth — so
      these routes ask it to change something and answer with what it then said.
      `provider_synced_at` is how stale the copy admits to being.
  - name: Provider webhooks
    description: >-
      Where a payment provider delivers to. Not a surface you call — it is a URL
      you paste into the provider's dashboard, which is why it sits outside
      `/v1`: a version bump must never mean editing a setting in somebody else's
      product. Signed with the secret of the connection named in the path, and
      safe to retry.
  - name: Entitlements
    description: >-
      What one of **your** customers may do, resolved: the plan their
      subscription carries, plus the overrides you promised them on top. This is
      the read your own gate calls, so it is cached and answers in one round
      trip. It is a different question from `GET /v1/organization/entitlements`,
      which is the plan **you** are on with UserKit — two catalogues, same word,
      different money.
paths:
  /v1/organization/catalogue/prices/{id}/reprice:
    post:
      tags:
        - Catalogue
      summary: 'Reprice: supersede a price'
      description: >-
        Requires `billing:write`. **This is how the money changes.** A price
        cannot be edited — there is no `PATCH` on one, and the database refuses
        an in-place edit of the amount, the currency, the kind, the recurrence,
        the tax posture or the plan.


        The reason is that a price is what somebody **agreed** to pay. A
        subscription refers to the price it was sold at, so editing the amount
        silently rewrites what an existing customer consented to and leaves no
        trace it was ever different.


        So repricing is: archive the old price, create the new one. Both happen
        here, in one transaction, in that order — and the order is not
        interchangeable. Only one live price may exist per plan per currency per
        recurrence, so creating first would be refused in the ordinary case (the
        same offer at a new number), and doing the two calls yourself leaves a
        window where the plan has no live price at all. A failure leaves the old
        price exactly where it was.


        The new price inherits the **plan** and nothing else: state the whole
        offer, currency included. Existing subscriptions keep the archived
        price; new customers get the new one.
      operationId: repriceCataloguePrice
      parameters:
        - $ref: '#/components/parameters/OrganizationHeader'
        - $ref: '#/components/parameters/EnvironmentQuery'
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The price being superseded. It must still be live.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CataloguePriceInput'
            example:
              kind: recurring
              currency: BRL
              amount_minor: 17900
              interval_unit: month
              interval_count: 1
              tax_behavior: inclusive
      responses:
        '201':
          description: >-
            Both rows: the new offer, and the one it superseded. The archived
            price comes back so a screen showing it does not have to re-read the
            catalogue to learn it is gone.
          content:
            application/json:
              schema:
                type: object
                properties:
                  price:
                    $ref: '#/components/schemas/CataloguePrice'
                  archived_price:
                    $ref: '#/components/schemas/CataloguePrice'
        '400':
          description: >-
            `invalid_request` — the same refusals as creating a price: no
            currency, a negative amount, no `tax_behavior`, or a recurrence that
            does not match the kind.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: >-
            `price_already_archived` — the price named has already been
            superseded, so there is nothing to supersede; or
            `offer_already_priced` — the new offer collides with a **different**
            live price of the same plan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  parameters:
    OrganizationHeader:
      name: X-Organization-Id
      in: header
      required: false
      schema:
        type: string
      description: >-
        The organization the caller is acting on — the `org_…` code that appears
        in the panel URL. It *identifies*; the membership JOIN is what
        *authorizes*, so a forged code reads nothing. Absent, the session's
        default organization answers.
    EnvironmentQuery:
      name: environment
      in: query
      required: false
      schema:
        type: string
        enum:
          - live
          - test
        default: live
      description: >-
        Which environment to act in. A view parameter, valid only on the staff
        surface — a machine credential never chooses its environment, it is
        resolved from the key.
  schemas:
    CataloguePriceInput:
      type: object
      description: >-
        A price on the way in — what both creating and repricing send. Every
        field is stated: there is no PATCH on a price, so no request carrying
        this is editing one, and "leave that field as it was" has no meaning
        here.
      required:
        - kind
        - currency
        - amount_minor
        - tax_behavior
      properties:
        kind:
          type: string
          enum:
            - recurring
            - one_time
        currency:
          type: string
          description: >-
            ISO 4217, uppercase. Required — an amount with no currency is not a
            price, and `1000` is R$10.00 in BRL and ¥1000 in JPY.
          example: BRL
        amount_minor:
          type: integer
          format: int64
          minimum: 0
          description: >-
            An integer in the currency's **minor unit**: `14900` is R$149.00.
            Never a decimal, and never divided by 100 on the way in — the
            currency's exponent is 0 for JPY and CLP and 3 for KWD.
        interval_unit:
          type: string
          enum:
            - day
            - week
            - month
            - year
          description: >-
            Required on a `recurring` price and **refused** on a `one_time` one,
            rather than ignored: a lifetime deal carrying a monthly interval is
            how it ends up in a renewal job's query.
        interval_count:
          type: integer
          minimum: 1
          description: >-
            How many intervals between charges. Required on `recurring`, refused
            on `one_time`.
        tax_behavior:
          type: string
          enum:
            - inclusive
            - exclusive
          description: >-
            Whether `amount_minor` already contains the tax. Required, with no
            third "undecided" value: we calculate no tax — that is the payment
            provider's job — but whether the number includes it is an input only
            the person who typed the number knows, and the moment it matters is
            the moment somebody is charged.
    CataloguePrice:
      type: object
      description: >-
        One plan's amount in one currency. A plan has many prices, one per
        currency: selling in BRL and USD is a price **per currency**, a number
        you chose, never a conversion at display time.


        A price is **immutable**. Repricing means creating a new price and
        archiving the old one, because a subscription refers to the price it was
        sold at and editing the amount would silently change what somebody
        agreed to pay.
      properties:
        id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - recurring
            - one_time
          description: >-
            `one_time` is a lifetime deal or a one-off purchase and carries no
            interval; `recurring` always carries one.
        currency:
          type: string
          description: >-
            ISO 4217, uppercase. Never absent — an amount with no currency is
            not a price.
          example: BRL
        amount_minor:
          type: integer
          format: int64
          description: >-
            The amount as an integer in the currency's **minor unit**. `14900`
            is R$149.00; in a zero-decimal currency the same integer is 14,900
            whole units.
        currency_exponent:
          type:
            - integer
            - 'null'
          description: >-
            How many decimal places the currency has, so `amount_minor` can be
            rendered without hard-coding a divisor: 2 for BRL and USD, 0 for JPY
            and CLP, 3 for KWD. `null` only if the stored code is one this API
            no longer prices in.
        interval_unit:
          type:
            - string
            - 'null'
          enum:
            - day
            - week
            - month
            - year
            - null
          description: '`null` on a `one_time` price.'
        interval_count:
          type:
            - integer
            - 'null'
          description: >-
            How many intervals between charges. `null` on a `one_time` price —
            never `0`.
        tax_behavior:
          type: string
          enum:
            - inclusive
            - exclusive
          description: >-
            Whether `amount_minor` already contains tax. There is no third
            “undecided” value: the calculation belongs to the payment provider,
            but this input is yours and is answered when the price is created.
        archived:
          type: boolean
          description: No longer sold. Existing subscriptions keep it.
        providers:
          type: array
          description: >-
            How this price is known at each payment provider. Empty means
            nothing can charge it yet. A price may be mapped onto more than one
            provider, and onto more than one account of the same provider.
          items:
            $ref: '#/components/schemas/ProviderPriceMapping'
        created_at:
          type: string
          format: date-time
    Error:
      type: object
      description: Every error in this API answers this envelope.
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Stable. Branch on this.
            message:
              type: string
              description: For a person. May change.
          required:
            - code
            - message
      required:
        - error
      example:
        error:
          code: forbidden
          message: your role does not allow this action
    ProviderPriceMapping:
      type: object
      properties:
        provider:
          type: string
          enum:
            - stripe
        provider_account_id:
          type: string
          description: >-
            The provider account the id belongs to. A provider's price id is
            unique only inside one account, which is why it is part of the
            mapping's identity.
        provider_price_id:
          type: string
        provider_product_id:
          type: string
          description: Empty where the provider has no product object above its prices.
  responses:
    Unauthorized:
      description: >-
        `unauthorized` — missing, malformed or expired credential. A well-formed
        credential from the wrong family says so: "this endpoint expects a staff
        session token, not an organization API key".
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: '`forbidden` — your role does not allow this action.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: >-
        `not_found`. Also the answer for a resource that exists in another
        organization or environment — the 404 never reveals which.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    sessionToken:
      type: http
      scheme: bearer
      description: >-
        A staff session token, `uk_st_…`. Minted by sign-up, sign-in or the
        two-factor exchange.

````