# Generated by infra/openapi-build.mjs from infra/openapi/*.mjs. Do not edit by hand.
# Served at https://pept.plus/openapi/peptplus-v1.openapi.yaml (JSON: .json, Postman: .postman_collection.json).
openapi: 3.1.0
info:
  title: PEPTPlus Partner API
  version: "2026-10-29"
  summary: REST API for providers, pharmacies and labs on PEPTPlus.
  description: "The PEPTPlus Partner API lets provider practices read the prescribable catalog and track orders,\nlets pharmacies run fulfillment and manage their catalog (products, lots, certificates of analysis),\nand lets labs read their API listings and licensees. Every organization can register webhook endpoints.\n\nAuthentication: `Authorization: Bearer sk_<org>_<env>_<48 hex>` where org is prov, pharm or lab and env is\nlive or test. Keys are minted in the portal account pages. Live keys require an active organization with a\nBusiness Associate Agreement on file (pharmacies also need a plan; the base plan is free). The sandbox at\ndemo.pept.plus mints test keys only and holds synthetic data.\n\nEvery response carries `X-Request-Id` and `X-API-Version`; authenticated responses add the `X-RateLimit-*`\nheaders. Errors use one envelope: `{ \"error\": { \"type\", \"message\", \"request_id\", ... } }`. Unknown paths\nunder /api/v1 return that envelope with type `not_found` (HTTP 404), never an HTML page.\n\nDedicated hosts: https://api.pept.plus/v1 (production) and https://sandbox.api.pept.plus/v1 (sandbox) are\nplanned and will serve the same routes with /v1 in place of /api/v1 once the DNS bindings are live.\n\nPrescription writes are deferred until EPCS certification: a controlled prescription must be signed by the\nprescriber with their own two-factor credential inside the certified application, which an API key that\nidentifies a server cannot do."
  contact:
    name: PEPTPlus developer support
    url: "https://pept.plus/developers"
  termsOfService: "https://pept.plus/legal/terms"
servers:
  - url: "https://pept.plus/api/v1"
    description: Production (live keys).
  - url: "https://demo.pept.plus/api/v1"
    description: Sandbox (test keys, synthetic data). sandbox.api.pept.plus will alias this host.
security:
  - bearerAuth: []
tags:
  - name: Catalog
    description: Prescribable compounds visible to the calling organization.
  - name: Orders
    description: Orders visible to the calling organization (provider or pharmacy).
  - name: Pharmacy fulfillment
    description: "The pharmacy fulfillment queue: accept, set status, post shipment."
  - name: Pharmacy catalog
    description: A pharmacy's own products, lots and certificates of analysis.
  - name: Labs
    description: A lab's API listings and the pharmacies licensing them.
  - name: Webhooks
    description: "Webhook endpoint management. The delivery contract is under the top-level `webhooks` object."
paths:
  /catalog:
    get:
      tags:
        - Catalog
      summary: List prescribable compounds
      description: "Compounds the calling organization can prescribe or list, with their strengths, routes and eligibility basis. Only compounds marked prescribable in the eligibility register are returned. Requires scope `catalog:read`. Allowed org types: provider_practice, pharmacy."
      operationId: listCatalog
      x-scope: "catalog:read"
      x-org-types:
        - provider_practice
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/limit"
        - "$ref": "#/components/parameters/starting_after"
      responses:
        "200":
          description: A page of compounds.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                  - has_more
                properties:
                  object:
                    type: string
                    const: list
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Compound"
                  has_more:
                    type: boolean
                    description: True when another page exists. Pass the last item's id as starting_after to fetch it.
              example:
                object: list
                data:
                  - id: cmp_5f2c7a10-3b4e-4d6f-9a8b-1c2d3e4f5a6b
                    object: compound
                    name: BPC-157
                    eligibility_basis: 503A
                    strengths:
                      - 5 mg
                    routes:
                      - subq
                    doses:
                      - value: "5"
                        unit: mg
                has_more: false
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /catalog/{id}:
    get:
      tags:
        - Catalog
      summary: Retrieve a compound
      description: "One compound by cmp_ id. Requires scope `catalog:read`. Allowed org types: provider_practice, pharmacy."
      operationId: getCompound
      x-scope: "catalog:read"
      x-org-types:
        - provider_practice
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/id"
      responses:
        "200":
          description: The compound.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Compound"
              example:
                id: cmp_5f2c7a10-3b4e-4d6f-9a8b-1c2d3e4f5a6b
                object: compound
                name: BPC-157
                eligibility_basis: 503A
                strengths:
                  - 5 mg
                routes:
                  - subq
                doses:
                  - value: "5"
                    unit: mg
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /orders:
    get:
      tags:
        - Orders
      summary: List orders
      description: "Orders visible to the calling organization: a provider key sees orders for its prescriptions, a pharmacy key sees orders routed to it. Reads are audited as patient-bearing (count only, no PHI). Requires scope `orders:read`. Allowed org types: provider_practice, pharmacy."
      operationId: listOrders
      x-scope: "orders:read"
      x-org-types:
        - provider_practice
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - name: status
          in: query
          required: false
          description: Filter by order status. Unknown values return 422.
          schema:
            type: string
            enum:
              - created
              - awaiting_payment
              - paid
              - sent_to_pharmacy
              - in_fulfillment
              - shipped
              - delivered
              - action_needed
              - canceled
        - "$ref": "#/components/parameters/limit"
        - "$ref": "#/components/parameters/starting_after"
      responses:
        "200":
          description: A page of orders, newest first.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                  - has_more
                properties:
                  object:
                    type: string
                    const: list
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Order"
                  has_more:
                    type: boolean
                    description: True when another page exists. Pass the last item's id as starting_after to fetch it.
              example:
                object: list
                data:
                  - id: ord_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
                    object: order
                    status: shipped
                    channel: web
                    patient: pt_8f3a1b2c-4d5e-4f60-a1b2-c3d4e5f6a7b8
                    compound: BPC-157
                    strength:
                      value: "5"
                      unit: mg
                    route: subq
                    amount:
                      patient_paid_cents: 14900
                      platform_fee_cents: 0
                      currency: usd
                    shipment:
                      carrier: USPS
                      tracking_no: "9400100000000000000000"
                      status: in_transit
                    created_at: "2026-10-28T17:02:11.000Z"
                    updated_at: "2026-10-29T14:30:00.000Z"
                has_more: true
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /orders/{id}:
    get:
      tags:
        - Orders
      summary: Retrieve an order
      description: "One order with its latest shipment. The read is audited with the order id and patient token. Requires scope `orders:read`. Allowed org types: provider_practice, pharmacy."
      operationId: getOrder
      x-scope: "orders:read"
      x-org-types:
        - provider_practice
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/id"
      responses:
        "200":
          description: The order.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Order"
              example:
                id: ord_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
                object: order
                status: shipped
                channel: web
                patient: pt_8f3a1b2c-4d5e-4f60-a1b2-c3d4e5f6a7b8
                compound: BPC-157
                strength:
                  value: "5"
                  unit: mg
                route: subq
                amount:
                  patient_paid_cents: 14900
                  platform_fee_cents: 0
                  currency: usd
                shipment:
                  carrier: USPS
                  tracking_no: "9400100000000000000000"
                  status: in_transit
                created_at: "2026-10-28T17:02:11.000Z"
                updated_at: "2026-10-29T14:30:00.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /listings:
    get:
      tags:
        - Labs
      summary: List your API listings
      description: "The lab's own API listings with the count of pharmacies licensing each one. Requires scope `listings:read`. Allowed org types: lab."
      operationId: listListings
      x-scope: "listings:read"
      x-org-types:
        - lab
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/limit"
        - "$ref": "#/components/parameters/starting_after"
      responses:
        "200":
          description: A page of listings.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                  - has_more
                properties:
                  object:
                    type: string
                    const: list
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Listing"
                  has_more:
                    type: boolean
                    description: True when another page exists. Pass the last item's id as starting_after to fetch it.
              example:
                object: list
                data:
                  - id: lst_3a91c0de-7f21-4b3a-9e55-0a1b2c3d4e5f
                    object: listing
                    substance: Sermorelin
                    monthly_fee: "499"
                    currency: usd
                    certified: true
                    eligibility_note: null
                    status: active
                    licensee_count: 4
                has_more: false
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /listings/{id}/licensees:
    get:
      tags:
        - Labs
      summary: List the licensees of a listing
      description: "Pharmacies licensing one of the lab's listings. Not paginated: the full list is returned without has_more, and limit and starting_after are ignored. 404 when the listing does not belong to the lab. Requires scope `licensees:read`. Allowed org types: lab."
      operationId: listListingLicensees
      x-scope: "licensees:read"
      x-org-types:
        - lab
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/id"
      responses:
        "200":
          description: All licenses on the listing.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                properties:
                  object:
                    type: string
                    const: list
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/License"
              example:
                object: list
                data:
                  - id: lic_0b7d2e4f-6a8c-4e1b-b3d5-7f9a1c3e5b7d
                    object: license
                    pharmacy: org_9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b
                    status: active
                    monthly_fee: "499"
                    currency: usd
                    started_at: "2026-09-01T00:00:00.000Z"
                    canceled_at: null
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /pharmacy/orders:
    get:
      tags:
        - Pharmacy fulfillment
      summary: List the fulfillment queue
      description: "Orders routed to the calling pharmacy, newest first. Same shape as GET /orders. Requires scope `pharmacy:fulfill`. Allowed org types: pharmacy."
      operationId: listPharmacyOrders
      x-scope: "pharmacy:fulfill"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - name: status
          in: query
          required: false
          description: Filter by order status. Unknown values return 422.
          schema:
            type: string
            enum:
              - created
              - awaiting_payment
              - paid
              - sent_to_pharmacy
              - in_fulfillment
              - shipped
              - delivered
              - action_needed
              - canceled
        - "$ref": "#/components/parameters/limit"
        - "$ref": "#/components/parameters/starting_after"
      responses:
        "200":
          description: A page of orders.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                  - has_more
                properties:
                  object:
                    type: string
                    const: list
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Order"
                  has_more:
                    type: boolean
                    description: True when another page exists. Pass the last item's id as starting_after to fetch it.
              example:
                object: list
                data:
                  - id: ord_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
                    object: order
                    status: paid
                    channel: web
                    patient: pt_8f3a1b2c-4d5e-4f60-a1b2-c3d4e5f6a7b8
                    compound: BPC-157
                    strength:
                      value: "5"
                      unit: mg
                    route: subq
                    amount:
                      patient_paid_cents: 14900
                      platform_fee_cents: 0
                      currency: usd
                    shipment: null
                    created_at: "2026-10-28T17:02:11.000Z"
                    updated_at: "2026-10-29T14:30:00.000Z"
                has_more: false
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /pharmacy/orders/{id}/accept:
    post:
      tags:
        - Pharmacy fulfillment
      summary: Accept an order
      description: "Acknowledge a routed order: the status moves to in_fulfillment. Empty body. A controlled order whose prescription fails signature or DEA verification, or that is blocked on transmission, is refused with 422 validation_error. Requires scope `pharmacy:fulfill`. Allowed org types: pharmacy. Honors `Idempotency-Key` (replays return the stored response with `Idempotent-Replayed: true`; a reused key with a different body returns 422 idempotency_error)."
      operationId: acceptOrder
      x-scope: "pharmacy:fulfill"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/Idempotency-Key"
        - "$ref": "#/components/parameters/id"
      responses:
        "200":
          description: The updated order.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Order"
              example:
                id: ord_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
                object: order
                status: in_fulfillment
                channel: web
                patient: pt_8f3a1b2c-4d5e-4f60-a1b2-c3d4e5f6a7b8
                compound: BPC-157
                strength:
                  value: "5"
                  unit: mg
                route: subq
                amount:
                  patient_paid_cents: 14900
                  platform_fee_cents: 0
                  currency: usd
                shipment: null
                created_at: "2026-10-28T17:02:11.000Z"
                updated_at: "2026-10-29T14:30:00.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /pharmacy/orders/{id}/status:
    post:
      tags:
        - Pharmacy fulfillment
      summary: Set an order status
      description: "Move the order to one of the pharmacy-settable statuses: in_fulfillment, shipped, delivered or action_needed. A controlled order whose prescription fails signature or DEA verification, or that is blocked on transmission, is refused with 422 validation_error. Requires scope `pharmacy:fulfill`. Allowed org types: pharmacy. Honors `Idempotency-Key` (replays return the stored response with `Idempotent-Replayed: true`; a reused key with a different body returns 422 idempotency_error)."
      operationId: setOrderStatus
      x-scope: "pharmacy:fulfill"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/Idempotency-Key"
        - "$ref": "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/OrderStatusBody"
            example:
              status: delivered
      responses:
        "200":
          description: The updated order.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Order"
              example:
                id: ord_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
                object: order
                status: delivered
                channel: web
                patient: pt_8f3a1b2c-4d5e-4f60-a1b2-c3d4e5f6a7b8
                compound: BPC-157
                strength:
                  value: "5"
                  unit: mg
                route: subq
                amount:
                  patient_paid_cents: 14900
                  platform_fee_cents: 0
                  currency: usd
                shipment:
                  carrier: USPS
                  tracking_no: "9400100000000000000000"
                  status: in_transit
                created_at: "2026-10-28T17:02:11.000Z"
                updated_at: "2026-10-29T14:30:00.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /pharmacy/orders/{id}/shipment:
    post:
      tags:
        - Pharmacy fulfillment
      summary: Record a shipment
      description: "Record the carrier and tracking number. The shipment is stored and the order advances to shipped. Requires scope `pharmacy:fulfill`. Allowed org types: pharmacy. Honors `Idempotency-Key` (replays return the stored response with `Idempotent-Replayed: true`; a reused key with a different body returns 422 idempotency_error)."
      operationId: createShipment
      x-scope: "pharmacy:fulfill"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/Idempotency-Key"
        - "$ref": "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ShipmentBody"
            example:
              carrier: USPS
              tracking_no: "9400100000000000000000"
      responses:
        "200":
          description: The updated order with the shipment embedded.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Order"
              example:
                id: ord_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
                object: order
                status: shipped
                channel: web
                patient: pt_8f3a1b2c-4d5e-4f60-a1b2-c3d4e5f6a7b8
                compound: BPC-157
                strength:
                  value: "5"
                  unit: mg
                route: subq
                amount:
                  patient_paid_cents: 14900
                  platform_fee_cents: 0
                  currency: usd
                shipment:
                  carrier: USPS
                  tracking_no: "9400100000000000000000"
                  status: in_transit
                created_at: "2026-10-28T17:02:11.000Z"
                updated_at: "2026-10-29T14:30:00.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /pharmacy/products:
    get:
      tags:
        - Pharmacy catalog
      summary: List your products
      description: "The pharmacy's own listings across both channels. Requires scope `pharmacy:catalog`. Allowed org types: pharmacy."
      operationId: listProducts
      x-scope: "pharmacy:catalog"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - name: channel
          in: query
          required: false
          schema:
            type: string
            enum:
              - patient_rx
              - office_stock
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - live
              - pending_review
              - draft
              - unlisted
              - hidden
        - "$ref": "#/components/parameters/limit"
        - "$ref": "#/components/parameters/starting_after"
      responses:
        "200":
          description: A page of products.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                  - has_more
                properties:
                  object:
                    type: string
                    const: list
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Product"
                  has_more:
                    type: boolean
                    description: True when another page exists. Pass the last item's id as starting_after to fetch it.
              example:
                object: list
                data:
                  - id: prd_1d2e3f40-5a6b-4c7d-8e9f-0a1b2c3d4e5f
                    object: product
                    compound: cmp_5f2c7a10-3b4e-4d6f-9a8b-1c2d3e4f5a6b
                    name: BPC-157
                    channel: patient_rx
                    status: live
                    published: true
                    delivery_method: subq
                    strength:
                      value: "5"
                      unit: mg
                    dosage_form_code: C42945
                    quantity_uom_code: C48542
                    pricing:
                      patient_price: "149.00"
                      price_basis: per_rx
                      currency: usd
                    latest_batch:
                      id: bat_7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d
                      lot_no: L-2041
                      quantity: 120
                      expiry_date: "2027-03-01"
                      coa_status: on_file
                    created_at: "2026-10-01T15:00:00.000Z"
                    updated_at: "2026-10-20T09:12:00.000Z"
                has_more: false
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
    post:
      tags:
        - Pharmacy catalog
      summary: Create a product
      description: "Add a listing for a compound from GET /catalog. The product is created pending review and goes live after PEPTPlus verification; published false saves a draft instead. A patient_rx product needs patient_price; an office_stock product needs pack_size, wholesale_price and lead_time_days. A compound that is not prescribable through the chosen channel returns 422. Requires scope `pharmacy:catalog`. Allowed org types: pharmacy. Honors `Idempotency-Key` (replays return the stored response with `Idempotent-Replayed: true`; a reused key with a different body returns 422 idempotency_error)."
      operationId: createProduct
      x-scope: "pharmacy:catalog"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/Idempotency-Key"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ProductCreate"
            example:
              compound: cmp_5f2c7a10-3b4e-4d6f-9a8b-1c2d3e4f5a6b
              channel: patient_rx
              delivery_method: subq
              strength:
                value: 5
                unit: mg
              dosage_form_code: C42945
              quantity_uom_code: C48542
              patient_price: 149
              price_basis: per_rx
              batch:
                lot_no: L-2041
                quantity: 120
                expiry_date: "2027-03-01"
      responses:
        "201":
          description: The created product.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Product"
              example:
                id: prd_1d2e3f40-5a6b-4c7d-8e9f-0a1b2c3d4e5f
                object: product
                compound: cmp_5f2c7a10-3b4e-4d6f-9a8b-1c2d3e4f5a6b
                name: BPC-157
                channel: patient_rx
                status: pending_review
                published: true
                delivery_method: subq
                strength:
                  value: "5"
                  unit: mg
                dosage_form_code: C42945
                quantity_uom_code: C48542
                pricing:
                  patient_price: "149.00"
                  price_basis: per_rx
                  currency: usd
                latest_batch:
                  id: bat_7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d
                  lot_no: L-2041
                  quantity: 120
                  expiry_date: "2027-03-01"
                  coa_status: on_file
                created_at: "2026-10-01T15:00:00.000Z"
                updated_at: "2026-10-20T09:12:00.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /pharmacy/products/{id}:
    get:
      tags:
        - Pharmacy catalog
      summary: Retrieve a product
      description: "One of the pharmacy's products by prd_ id. Requires scope `pharmacy:catalog`. Allowed org types: pharmacy."
      operationId: getProduct
      x-scope: "pharmacy:catalog"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/id"
      responses:
        "200":
          description: The product.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Product"
              example:
                id: prd_1d2e3f40-5a6b-4c7d-8e9f-0a1b2c3d4e5f
                object: product
                compound: cmp_5f2c7a10-3b4e-4d6f-9a8b-1c2d3e4f5a6b
                name: BPC-157
                channel: patient_rx
                status: live
                published: true
                delivery_method: subq
                strength:
                  value: "5"
                  unit: mg
                dosage_form_code: C42945
                quantity_uom_code: C48542
                pricing:
                  patient_price: "149.00"
                  price_basis: per_rx
                  currency: usd
                latest_batch:
                  id: bat_7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d
                  lot_no: L-2041
                  quantity: 120
                  expiry_date: "2027-03-01"
                  coa_status: on_file
                created_at: "2026-10-01T15:00:00.000Z"
                updated_at: "2026-10-20T09:12:00.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
    patch:
      tags:
        - Pharmacy catalog
      summary: Update a product
      description: "Change pricing, availability or form fields. published true shows an approved product in the marketplace and false unlists it. Changing delivery_method on an approved product sends it back for review; the response says so in sent_to_review. Requires scope `pharmacy:catalog`. Allowed org types: pharmacy. Honors `Idempotency-Key` (replays return the stored response with `Idempotent-Replayed: true`; a reused key with a different body returns 422 idempotency_error)."
      operationId: updateProduct
      x-scope: "pharmacy:catalog"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/Idempotency-Key"
        - "$ref": "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ProductPatch"
            example:
              patient_price: "159.00"
              published: true
      responses:
        "200":
          description: The updated product.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ProductPatched"
              example:
                id: prd_1d2e3f40-5a6b-4c7d-8e9f-0a1b2c3d4e5f
                object: product
                compound: cmp_5f2c7a10-3b4e-4d6f-9a8b-1c2d3e4f5a6b
                name: BPC-157
                channel: patient_rx
                status: live
                published: true
                delivery_method: subq
                strength:
                  value: "5"
                  unit: mg
                dosage_form_code: C42945
                quantity_uom_code: C48542
                pricing:
                  patient_price: "159.00"
                  price_basis: per_rx
                  currency: usd
                latest_batch:
                  id: bat_7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d
                  lot_no: L-2041
                  quantity: 120
                  expiry_date: "2027-03-01"
                  coa_status: on_file
                created_at: "2026-10-01T15:00:00.000Z"
                updated_at: "2026-10-20T09:12:00.000Z"
                sent_to_review: false
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /pharmacy/batches:
    get:
      tags:
        - Pharmacy catalog
      summary: List lots
      description: "Lots (batches) across the pharmacy's products, each with its latest certificate of analysis. Requires scope `pharmacy:catalog`. Allowed org types: pharmacy."
      operationId: listBatches
      x-scope: "pharmacy:catalog"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - name: product
          in: query
          required: false
          description: Only lots of this prd_ id.
          schema:
            type: string
        - "$ref": "#/components/parameters/limit"
        - "$ref": "#/components/parameters/starting_after"
      responses:
        "200":
          description: A page of lots.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                  - has_more
                properties:
                  object:
                    type: string
                    const: list
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Batch"
                  has_more:
                    type: boolean
                    description: True when another page exists. Pass the last item's id as starting_after to fetch it.
              example:
                object: list
                data:
                  - id: bat_7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d
                    object: batch
                    product: prd_1d2e3f40-5a6b-4c7d-8e9f-0a1b2c3d4e5f
                    compound: BPC-157
                    strength:
                      value: "5"
                      unit: mg
                    lot_no: L-2041
                    quantity: 120
                    mfg_date: "2026-09-15"
                    expiry_date: "2027-03-01"
                    coa:
                      id: coa_2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f
                      object: coa
                      status: on_file
                      purity_pct: "99.2"
                      lab_name: Janoshik
                      tested_on: "2026-09-20"
                      uploaded_at: "2026-10-02T11:00:00.000Z"
                    created_at: "2026-10-01T15:00:00.000Z"
                has_more: false
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
    post:
      tags:
        - Pharmacy catalog
      summary: Create a lot
      description: "Add a lot to one of the pharmacy's products. Requires scope `pharmacy:catalog`. Allowed org types: pharmacy. Honors `Idempotency-Key` (replays return the stored response with `Idempotent-Replayed: true`; a reused key with a different body returns 422 idempotency_error)."
      operationId: createBatch
      x-scope: "pharmacy:catalog"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/Idempotency-Key"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/BatchCreate"
            example:
              product: prd_1d2e3f40-5a6b-4c7d-8e9f-0a1b2c3d4e5f
              lot_no: L-2042
              quantity: 80
              mfg_date: "2026-10-10"
              expiry_date: "2027-04-01"
      responses:
        "201":
          description: The created lot.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Batch"
              example:
                id: bat_7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d
                object: batch
                product: prd_1d2e3f40-5a6b-4c7d-8e9f-0a1b2c3d4e5f
                compound: BPC-157
                strength:
                  value: "5"
                  unit: mg
                lot_no: L-2042
                quantity: 120
                mfg_date: "2026-09-15"
                expiry_date: "2027-03-01"
                coa: null
                created_at: "2026-10-01T15:00:00.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /pharmacy/batches/{id}:
    get:
      tags:
        - Pharmacy catalog
      summary: Retrieve a lot
      description: "One lot with its latest certificate of analysis. Requires scope `pharmacy:catalog`. Allowed org types: pharmacy."
      operationId: getBatch
      x-scope: "pharmacy:catalog"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/id"
      responses:
        "200":
          description: The lot.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Batch"
              example:
                id: bat_7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d
                object: batch
                product: prd_1d2e3f40-5a6b-4c7d-8e9f-0a1b2c3d4e5f
                compound: BPC-157
                strength:
                  value: "5"
                  unit: mg
                lot_no: L-2041
                quantity: 120
                mfg_date: "2026-09-15"
                expiry_date: "2027-03-01"
                coa:
                  id: coa_2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f
                  object: coa
                  status: on_file
                  purity_pct: "99.2"
                  lab_name: Janoshik
                  tested_on: "2026-09-20"
                  uploaded_at: "2026-10-02T11:00:00.000Z"
                created_at: "2026-10-01T15:00:00.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /pharmacy/batches/{id}/coa:
    post:
      tags:
        - Pharmacy catalog
      summary: Upload a certificate of analysis
      description: "Attach a COA PDF (at most 15 MB) to a lot, as multipart/form-data with a `file` field or as JSON with `pdf_base64`. Purity, lab name and test date are stored as sent; nothing is parsed from the PDF. Idempotency-Key is not honored on this route. When file storage is not configured the route answers 503 with a flat `{ \"error\": \"storage_unavailable\", \"message\": ... }` body rather than the standard envelope. Requires scope `pharmacy:catalog`. Allowed org types: pharmacy."
      operationId: uploadCoa
      x-scope: "pharmacy:catalog"
      x-org-types:
        - pharmacy
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/id"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              "$ref": "#/components/schemas/CoaMultipart"
          application/json:
            schema:
              "$ref": "#/components/schemas/CoaJson"
            example:
              pdf_base64: JVBERi0xLjQKJ...
              purity_pct: 99.2
              lab_name: Janoshik
              tested_on: "2026-09-20"
      responses:
        "201":
          description: The lot with the new certificate.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Batch"
              example:
                id: bat_7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d
                object: batch
                product: prd_1d2e3f40-5a6b-4c7d-8e9f-0a1b2c3d4e5f
                compound: BPC-157
                strength:
                  value: "5"
                  unit: mg
                lot_no: L-2041
                quantity: 120
                mfg_date: "2026-09-15"
                expiry_date: "2027-03-01"
                coa:
                  id: coa_2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f
                  object: coa
                  status: on_file
                  purity_pct: "99.2"
                  lab_name: Janoshik
                  tested_on: "2026-09-20"
                  uploaded_at: "2026-10-02T11:00:00.000Z"
                created_at: "2026-10-01T15:00:00.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
        "503":
          description: File storage is not configured on this host.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    const: storage_unavailable
                  message:
                    type: string
  /webhooks:
    get:
      tags:
        - Webhooks
      summary: List webhook endpoints
      description: "The organization's registered endpoints. Not paginated: has_more is always false. Requires scope `webhooks:manage`. Allowed org types: provider_practice, pharmacy, lab."
      operationId: listWebhookEndpoints
      x-scope: "webhooks:manage"
      x-org-types:
        - provider_practice
        - pharmacy
        - lab
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
      responses:
        "200":
          description: All endpoints.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                  - has_more
                properties:
                  object:
                    type: string
                    const: list
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/WebhookEndpoint"
                  has_more:
                    type: boolean
                    description: True when another page exists. Pass the last item's id as starting_after to fetch it.
              example:
                object: list
                data:
                  - id: whe_4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8
                    object: webhook_endpoint
                    url: "https://example.com/peptplus/webhooks"
                    description: Pharmacy system, production
                    events:
                      - order.created
                      - order.status_changed
                      - shipment.created
                    active: true
                    disabled_reason: null
                    secret_hint: 9c4e
                    stats:
                      delivered: 128
                      failed: 2
                      consecutive_failures: 0
                      last_delivered_at: "2026-10-29T14:31:02.000Z"
                    created_at: "2026-10-29T10:00:00.000Z"
                    updated_at: "2026-10-29T14:31:02.000Z"
                has_more: false
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
    post:
      tags:
        - Webhooks
      summary: Create a webhook endpoint
      description: "Register an https endpoint for the given events. The response carries `secret` exactly once; store it. At most 10 endpoints per organization. Requires scope `webhooks:manage`. Allowed org types: provider_practice, pharmacy, lab. Honors `Idempotency-Key` (replays return the stored response with `Idempotent-Replayed: true`; a reused key with a different body returns 422 idempotency_error)."
      operationId: createWebhookEndpoint
      x-scope: "webhooks:manage"
      x-org-types:
        - provider_practice
        - pharmacy
        - lab
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/Idempotency-Key"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/WebhookEndpointCreate"
            example:
              url: "https://example.com/peptplus/webhooks"
              description: Pharmacy system, production
              events:
                - order.created
                - order.status_changed
                - shipment.created
      responses:
        "201":
          description: The endpoint, with its signing secret.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/WebhookEndpoint"
              example:
                id: whe_4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8
                object: webhook_endpoint
                url: "https://example.com/peptplus/webhooks"
                description: Pharmacy system, production
                events:
                  - order.created
                  - order.status_changed
                  - shipment.created
                active: true
                disabled_reason: null
                secret_hint: 9c4e
                stats:
                  delivered: 0
                  failed: 0
                  consecutive_failures: 0
                  last_delivered_at: null
                created_at: "2026-10-29T10:00:00.000Z"
                updated_at: "2026-10-29T14:31:02.000Z"
                secret: whsec_0123456789abcdef0123456789abcdef0123456789ab9c4e
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "422":
          "$ref": "#/components/responses/Unprocessable"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /webhooks/{id}:
    get:
      tags:
        - Webhooks
      summary: Retrieve a webhook endpoint
      description: "One endpoint with its 20 most recent deliveries. Poll this after POST /webhooks/{id}/test to see the ping result. Requires scope `webhooks:manage`. Allowed org types: provider_practice, pharmacy, lab."
      operationId: getWebhookEndpoint
      x-scope: "webhooks:manage"
      x-org-types:
        - provider_practice
        - pharmacy
        - lab
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/id"
      responses:
        "200":
          description: The endpoint and recent deliveries.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/WebhookEndpointWithDeliveries"
              example:
                id: whe_4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8
                object: webhook_endpoint
                url: "https://example.com/peptplus/webhooks"
                description: Pharmacy system, production
                events:
                  - order.created
                  - order.status_changed
                  - shipment.created
                active: true
                disabled_reason: null
                secret_hint: 9c4e
                stats:
                  delivered: 128
                  failed: 2
                  consecutive_failures: 0
                  last_delivered_at: "2026-10-29T14:31:02.000Z"
                created_at: "2026-10-29T10:00:00.000Z"
                updated_at: "2026-10-29T14:31:02.000Z"
                recent_deliveries:
                  - id: whd_0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d
                    object: webhook_delivery
                    endpoint: whe_4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8
                    event: evt_2a2f3b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
                    type: order.created
                    status: delivered
                    attempt_count: 1
                    next_attempt_at: null
                    last_attempt_at: "2026-10-29T14:31:02.000Z"
                    last_status_code: 200
                    last_error: null
                    delivered_at: "2026-10-29T14:31:02.000Z"
                    replay_of: null
                    created_at: "2026-10-29T14:30:10.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
    delete:
      tags:
        - Webhooks
      summary: Delete a webhook endpoint
      description: "Remove the endpoint. Pending deliveries are dropped; the delivery log is kept. Requires scope `webhooks:manage`. Allowed org types: provider_practice, pharmacy, lab. Honors `Idempotency-Key` (replays return the stored response with `Idempotent-Replayed: true`; a reused key with a different body returns 422 idempotency_error)."
      operationId: deleteWebhookEndpoint
      x-scope: "webhooks:manage"
      x-org-types:
        - provider_practice
        - pharmacy
        - lab
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/Idempotency-Key"
        - "$ref": "#/components/parameters/id"
      responses:
        "200":
          description: Confirmation.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Deleted"
              example:
                id: whe_4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8
                object: webhook_endpoint
                deleted: true
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
  /webhooks/{id}/test:
    post:
      tags:
        - Webhooks
      summary: Send a test event
      description: "Queue a `ping` event for the endpoint. The dispatcher runs every 15 minutes, so the delivery is pending until its next run. Idempotency-Key is not honored on this route. Requires scope `webhooks:manage`. Allowed org types: provider_practice, pharmacy, lab."
      operationId: testWebhookEndpoint
      x-scope: "webhooks:manage"
      x-org-types:
        - provider_practice
        - pharmacy
        - lab
      parameters:
        - "$ref": "#/components/parameters/X-Request-Id"
        - "$ref": "#/components/parameters/id"
      responses:
        "202":
          description: The queued delivery.
          headers:
            X-Request-Id:
              "$ref": "#/components/headers/X-Request-Id"
            X-API-Version:
              "$ref": "#/components/headers/X-API-Version"
            X-RateLimit-Limit:
              "$ref": "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              "$ref": "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              "$ref": "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/WebhookDelivery"
              example:
                id: whd_0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d
                object: webhook_delivery
                endpoint: whe_4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8
                event: evt_2a2f3b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
                type: ping
                status: pending
                attempt_count: 0
                next_attempt_at: "2026-10-29T14:45:00.000Z"
                last_attempt_at: null
                last_status_code: null
                last_error: null
                delivered_at: null
                replay_of: null
                created_at: "2026-10-29T14:30:10.000Z"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
        "429":
          "$ref": "#/components/responses/RateLimited"
        "500":
          "$ref": "#/components/responses/Internal"
webhooks:
  event:
    post:
      summary: Event delivery
      description: "PEPTPlus POSTs each event to every subscribed endpoint with Content-Type application/json and the headers PEPTPlus-Signature (t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" using the endpoint secret), PEPTPlus-Event (the type), PEPTPlus-Event-Id (evt_ id, stable across endpoints and replays), PEPTPlus-Delivery (whd_ id per attempt) and User-Agent PEPTPlus-Webhooks/1.0. Respond with any 2xx within 10 seconds; redirects are not followed. A failed attempt is retried 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours after the previous one, then marked dead. After 50 consecutive failures the endpoint is disabled until re-enabled in the portal. Verify the signature over the raw bytes, compare in constant time and reject timestamps older than your tolerance (5 minutes suggested)."
      parameters:
        - name: PEPTPlus-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: "^t=\\d+,v1=[0-9a-f]{64}$"
        - name: PEPTPlus-Event
          in: header
          required: true
          schema:
            type: string
        - name: PEPTPlus-Event-Id
          in: header
          required: true
          schema:
            type: string
        - name: PEPTPlus-Delivery
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/WebhookEvent"
            example:
              id: evt_2a2f3b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
              object: event
              type: order.status_changed
              created: 1761750000
              api_version: "2026-10-29"
              data:
                order:
                  id: ord_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
                  object: order
                  status: in_fulfillment
                  channel: web
                  patient: pt_8f3a1b2c-4d5e-4f60-a1b2-c3d4e5f6a7b8
                  compound: BPC-157
                  strength:
                    value: "5"
                    unit: mg
                  route: subq
                  amount:
                    patient_paid_cents: 14900
                    platform_fee_cents: 0
                    currency: usd
                  shipment: null
                  created_at: "2026-10-28T17:02:11.000Z"
                  updated_at: "2026-10-29T14:30:00.000Z"
                previous_status: paid
                source: pharmacy
      responses:
        2XX:
          description: Delivered. Any 2xx status counts.
        default:
          description: Any other status, a timeout or a connection error schedules a retry.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_<org>_<env>_<48 hex>
      description: "API key minted in the portal. Example: sk_pharm_live_0123456789abcdef... (48 hex characters after the env marker)."
  headers:
    X-Request-Id:
      description: "Request id. Echoes the inbound X-Request-Id when it matches ^[A-Za-z0-9._:-]{1,128}$, otherwise a fresh UUID."
      schema:
        type: string
    X-API-Version:
      description: Date of the API contract this response follows.
      schema:
        type: string
        const: "2026-10-29"
    X-RateLimit-Limit:
      description: Requests allowed per 60 second window for this key (default 600).
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Requests left in the current window.
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Unix seconds when the current window ends.
      schema:
        type: integer
    Retry-After:
      description: Seconds to wait before retrying (429 only).
      schema:
        type: integer
    Idempotent-Replayed:
      description: Present with the value true when the body is the stored response for the Idempotency-Key.
      schema:
        type: string
        const: "true"
    Deprecation:
      description: RFC 9745 marker (@<unix seconds>) on a route that has been deprecated. No route is deprecated today.
      schema:
        type: string
    Sunset:
      description: RFC 8594 HTTP-date after which a deprecated route stops working.
      schema:
        type: string
  parameters:
    X-Request-Id:
      name: X-Request-Id
      in: header
      required: false
      description: "Optional correlation id (1 to 128 characters from A-Z a-z 0-9 . _ : -). Echoed in the response header and the error envelope."
      schema:
        type: string
        pattern: "^[A-Za-z0-9._:-]{1,128}$"
    Idempotency-Key:
      name: Idempotency-Key
      in: header
      required: false
      description: "Optional, 1 to 255 characters. The first request runs and its status and body are stored for 24 hours, scoped to the key. The same key with the same method, path and body replays the stored response; a different body returns 422 idempotency_error. Responses of 500 and above are not stored."
      schema:
        type: string
        minLength: 1
        maxLength: 255
    limit:
      name: limit
      in: query
      required: false
      description: Page size, 1 to 100 (default 25). Values above 100 are clamped to 100; zero, negative or non-integer values return 422.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    starting_after:
      name: starting_after
      in: query
      required: false
      description: "Cursor: the id of the last item of the previous page (prefixed or bare UUID). Lists are ordered newest first by (created_at, id). An id that is not visible to the key returns 422."
      schema:
        type: string
    id:
      name: id
      in: path
      required: true
      description: Object id, prefixed (for example ord_...) or the bare UUID.
      schema:
        type: string
  responses:
    Unauthorized:
      description: "Missing, malformed, unknown, revoked or environment-mismatched key, a key whose org type cannot use the API, or an organization that is not active (pending or suspended)."
      headers:
        X-Request-Id:
          "$ref": "#/components/headers/X-Request-Id"
        X-API-Version:
          "$ref": "#/components/headers/X-API-Version"
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error:
              type: authentication_error
              message: Unknown or revoked API key.
              request_id: 2b1f6d0e-4a1c-4b7e-9f3a-0c8d7e6f5a4b
    Forbidden:
      description: "The key lacks the required scope (the envelope carries `required`), or the key's org type cannot call this endpoint."
      headers:
        X-Request-Id:
          "$ref": "#/components/headers/X-Request-Id"
        X-API-Version:
          "$ref": "#/components/headers/X-API-Version"
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error:
              type: permission_error
              message: "This key lacks the required scope: orders:read."
              request_id: 2b1f6d0e-4a1c-4b7e-9f3a-0c8d7e6f5a4b
              required: "orders:read"
    NotFound:
      description: No such object visible to this organization, or no such API route.
      headers:
        X-Request-Id:
          "$ref": "#/components/headers/X-Request-Id"
        X-API-Version:
          "$ref": "#/components/headers/X-API-Version"
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error:
              type: not_found
              message: Order not found.
              request_id: 2b1f6d0e-4a1c-4b7e-9f3a-0c8d7e6f5a4b
    Unprocessable:
      description: "validation_error for a bad body or query parameter (`details` carries field errors when a schema failed), or idempotency_error when an Idempotency-Key is reused with a different body."
      headers:
        X-Request-Id:
          "$ref": "#/components/headers/X-Request-Id"
        X-API-Version:
          "$ref": "#/components/headers/X-API-Version"
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error:
              type: validation_error
              message: Invalid request body.
              request_id: 2b1f6d0e-4a1c-4b7e-9f3a-0c8d7e6f5a4b
              details:
                formErrors: []
                fieldErrors:
                  carrier:
                    - Required
    RateLimited:
      description: The key exceeded its per-minute limit (default 600), or an IP made more than 60 failed authentications in a minute.
      headers:
        X-Request-Id:
          "$ref": "#/components/headers/X-Request-Id"
        X-API-Version:
          "$ref": "#/components/headers/X-API-Version"
        Retry-After:
          "$ref": "#/components/headers/Retry-After"
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error:
              type: rate_limit_error
              message: Too many requests. Slow down and retry after the window resets.
              request_id: 2b1f6d0e-4a1c-4b7e-9f3a-0c8d7e6f5a4b
              retry_after: 17
    Internal:
      description: "Unexpected failure. Retry with the same Idempotency-Key where one was sent: 5xx responses are never stored for replay."
      headers:
        X-Request-Id:
          "$ref": "#/components/headers/X-Request-Id"
        X-API-Version:
          "$ref": "#/components/headers/X-API-Version"
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error:
              type: internal_error
              message: Internal error.
              request_id: 2b1f6d0e-4a1c-4b7e-9f3a-0c8d7e6f5a4b
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - type
            - message
            - request_id
          properties:
            type:
              type: string
              enum:
                - authentication_error
                - permission_error
                - not_found
                - validation_error
                - rate_limit_error
                - idempotency_error
                - internal_error
              description: "authentication_error 401, permission_error 403, not_found 404, validation_error 422, idempotency_error 422, rate_limit_error 429, internal_error 500."
            message:
              type: string
              description: Human readable explanation.
            request_id:
              type:
                - string
                - "null"
              description: Same value as the X-Request-Id header.
            required:
              type: string
              description: "permission_error only: the scope the key lacks."
            details:
              description: "validation_error only: the field errors (zod flatten shape: formErrors and fieldErrors)."
            retry_after:
              type: integer
              description: "rate_limit_error only: seconds until the window resets."
    Batch:
      type: object
      required:
        - id
        - object
        - product
        - compound
        - strength
        - lot_no
        - quantity
        - mfg_date
        - expiry_date
        - coa
        - created_at
      properties:
        id:
          type: string
          pattern: "^bat_[0-9a-f-]{36}$"
          description: Object id with the bat_ prefix.
          example: bat_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: batch
        product:
          type: string
          pattern: "^prd_[0-9a-f-]{36}$"
          description: Object id with the prd_ prefix.
          example: prd_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        compound:
          type:
            - string
            - "null"
        strength:
          oneOf:
            - "$ref": "#/components/schemas/Strength"
            - type: "null"
        lot_no:
          type:
            - string
            - "null"
        quantity:
          type:
            - integer
            - "null"
        mfg_date:
          type:
            - string
            - "null"
          description: YYYY-MM-DD.
        expiry_date:
          type:
            - string
            - "null"
          description: YYYY-MM-DD.
        coa:
          oneOf:
            - "$ref": "#/components/schemas/Coa"
            - type: "null"
          description: Latest certificate of analysis, or null.
        created_at:
          type: string
          format: date-time
    BatchCreate:
      type: object
      required:
        - product
        - lot_no
      properties:
        product:
          type: string
          description: prd_ id (or bare UUID).
        lot_no:
          type: string
          minLength: 1
          maxLength: 64
        quantity:
          type: integer
          minimum: 0
        mfg_date:
          type: string
          pattern: "^(\\d{1,2}/\\d{4}|\\d{4}-\\d{2}(-\\d{2})?)$"
          description: MM/YYYY, YYYY-MM or YYYY-MM-DD.
        expiry_date:
          type: string
          pattern: "^(\\d{1,2}/\\d{4}|\\d{4}-\\d{2}(-\\d{2})?)$"
          description: MM/YYYY, YYYY-MM or YYYY-MM-DD.
    BatchVerifiedData:
      type: object
      required:
        - product
        - batch
      description: "batch.verified (pharmacy only): PEPTPlus verified the listing."
      properties:
        product:
          type: object
          required:
            - id
            - object
            - compound
            - status
          properties:
            id:
              type: string
              pattern: "^prd_[0-9a-f-]{36}$"
              description: Object id with the prd_ prefix.
              example: prd_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
            object:
              type: string
              const: product
            compound:
              type: string
              pattern: "^cmp_[0-9a-f-]{36}$"
              description: Object id with the cmp_ prefix.
              example: cmp_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
            status:
              type: string
              const: live
        batch:
          oneOf:
            - type: object
              required:
                - id
                - object
                - lot_no
                - expiry_date
                - coa_on_file
              properties:
                id:
                  type: string
                  pattern: "^bat_[0-9a-f-]{36}$"
                  description: Object id with the bat_ prefix.
                  example: bat_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
                object:
                  type: string
                  const: batch
                lot_no:
                  type:
                    - string
                    - "null"
                expiry_date:
                  type:
                    - string
                    - "null"
                coa_on_file:
                  type: boolean
            - type: "null"
    Coa:
      type: object
      required:
        - id
        - object
        - status
        - purity_pct
        - lab_name
        - tested_on
        - uploaded_at
      properties:
        id:
          type: string
          pattern: "^coa_[0-9a-f-]{36}$"
          description: Object id with the coa_ prefix.
          example: coa_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: coa
        status:
          type:
            - string
            - "null"
          description: on_file once uploaded.
        purity_pct:
          type:
            - string
            - "null"
        lab_name:
          type:
            - string
            - "null"
        tested_on:
          type:
            - string
            - "null"
          description: YYYY-MM-DD.
        uploaded_at:
          type:
            - string
            - "null"
          format: date-time
    CoaJson:
      type: object
      required:
        - pdf_base64
      properties:
        pdf_base64:
          type: string
          description: "The PDF as base64 (a data:application/pdf;base64, prefix is accepted). Decoded size 1 byte to 15 MB."
          minLength: 16
        purity_pct:
          oneOf:
            - type: number
              minimum: 0
              maximum: 100
            - type: string
              pattern: "^\\d+(\\.\\d+)?$"
        lab_name:
          type: string
          maxLength: 200
        tested_on:
          type: string
          pattern: "^(\\d{1,2}/\\d{4}|\\d{4}-\\d{2}(-\\d{2})?)$"
          description: MM/YYYY, YYYY-MM or YYYY-MM-DD.
    CoaMultipart:
      type: object
      required:
        - file
      properties:
        file:
          type: string
          format: binary
          description: The PDF, at most 15 MB.
        purity_pct:
          type: string
        lab_name:
          type: string
        tested_on:
          type: string
          pattern: "^(\\d{1,2}/\\d{4}|\\d{4}-\\d{2}(-\\d{2})?)$"
          description: MM/YYYY, YYYY-MM or YYYY-MM-DD.
    Compound:
      type: object
      required:
        - id
        - object
        - name
        - eligibility_basis
        - strengths
        - routes
        - doses
      properties:
        id:
          type: string
          pattern: "^cmp_[0-9a-f-]{36}$"
          description: Object id with the cmp_ prefix.
          example: cmp_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: compound
        name:
          type: string
          description: Display name, for example BPC-157.
        eligibility_basis:
          type:
            - string
            - "null"
          description: Why the compound is prescribable (for example 503A).
        strengths:
          type: array
          items:
            type: string
          description: "Human readable strengths available, for example \"5 mg\"."
        routes:
          type: array
          items:
            type: string
          description: Delivery routes available, for example subq.
        doses:
          type: array
          items:
            type: object
            required:
              - value
              - unit
            properties:
              value:
                type: string
              unit:
                type: string
    Deleted:
      type: object
      required:
        - id
        - object
        - deleted
      properties:
        id:
          type: string
          pattern: "^whe_[0-9a-f-]{36}$"
          description: Object id with the whe_ prefix.
          example: whe_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: webhook_endpoint
        deleted:
          type: boolean
          const: true
    License:
      type: object
      required:
        - id
        - object
        - pharmacy
        - status
        - monthly_fee
        - currency
        - started_at
        - canceled_at
      properties:
        id:
          type: string
          pattern: "^lic_[0-9a-f-]{36}$"
          description: Object id with the lic_ prefix.
          example: lic_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: license
        pharmacy:
          type: string
          pattern: "^org_[0-9a-f-]{36}$"
          description: The licensing pharmacy. Only the id is visible to the lab.
          example: org_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        status:
          type: string
          enum:
            - active
            - canceled
        monthly_fee:
          type:
            - string
            - "null"
        currency:
          type: string
          const: usd
        started_at:
          type: string
          format: date-time
        canceled_at:
          type:
            - string
            - "null"
          format: date-time
    Listing:
      type: object
      required:
        - id
        - object
        - substance
        - monthly_fee
        - currency
        - certified
        - eligibility_note
        - status
        - licensee_count
      properties:
        id:
          type: string
          pattern: "^lst_[0-9a-f-]{36}$"
          description: Object id with the lst_ prefix.
          example: lst_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: listing
        substance:
          type: string
        monthly_fee:
          type:
            - string
            - "null"
          description: Decimal string in dollars.
        currency:
          type: string
          const: usd
        certified:
          type: boolean
        eligibility_note:
          type:
            - string
            - "null"
        status:
          type: string
          enum:
            - active
            - coming
        licensee_count:
          type: integer
          description: Active licenses against this listing.
    ListingLicensedData:
      type: object
      required:
        - license
      description: listing.licensed (lab only).
      properties:
        license:
          type: object
          required:
            - id
            - object
            - listing
            - pharmacy
            - monthly_fee
            - status
            - started_at
          properties:
            id:
              type: string
              pattern: "^lic_[0-9a-f-]{36}$"
              description: Object id with the lic_ prefix.
              example: lic_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
            object:
              type: string
              const: license
            listing:
              type: string
              pattern: "^lst_[0-9a-f-]{36}$"
              description: Object id with the lst_ prefix.
              example: lst_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
            pharmacy:
              type: string
              pattern: "^org_[0-9a-f-]{36}$"
              description: Object id with the org_ prefix.
              example: org_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
            monthly_fee:
              type:
                - string
                - "null"
            status:
              type: string
            started_at:
              type: string
              format: date-time
    Order:
      type: object
      required:
        - id
        - object
        - status
        - channel
        - patient
        - compound
        - strength
        - route
        - amount
        - shipment
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^ord_[0-9a-f-]{36}$"
          description: Object id with the ord_ prefix.
          example: ord_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: order
        status:
          type: string
          enum:
            - created
            - awaiting_payment
            - paid
            - sent_to_pharmacy
            - in_fulfillment
            - shipped
            - delivered
            - action_needed
            - canceled
        channel:
          type: string
          description: How the order was created, for example web or api.
        patient:
          type:
            - string
            - "null"
          pattern: "^pt_[0-9a-f-]{36}$"
          description: Patient token. The only patient identifier the API ever returns.
          example: pt_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        compound:
          type:
            - string
            - "null"
          description: Compound display name.
        strength:
          oneOf:
            - "$ref": "#/components/schemas/Strength"
            - type: "null"
        route:
          type:
            - string
            - "null"
        amount:
          type: object
          required:
            - patient_paid_cents
            - platform_fee_cents
            - currency
          properties:
            patient_paid_cents:
              type:
                - integer
                - "null"
            platform_fee_cents:
              type:
                - integer
                - "null"
            currency:
              type: string
              const: usd
        shipment:
          oneOf:
            - type: object
              required:
                - carrier
                - tracking_no
                - status
              properties:
                carrier:
                  type:
                    - string
                    - "null"
                tracking_no:
                  type:
                    - string
                    - "null"
                status:
                  type: string
            - type: "null"
          description: Latest shipment, or null before one is recorded.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    OrderEventData:
      type: object
      required:
        - order
      description: order.created, order.accepted, order.status_changed, shipment.created, shipment.delivered.
      properties:
        order:
          "$ref": "#/components/schemas/Order"
        previous_status:
          type:
            - string
            - "null"
          enum:
            - created
            - awaiting_payment
            - paid
            - sent_to_pharmacy
            - in_fulfillment
            - shipped
            - delivered
            - action_needed
            - canceled
            - null
          description: order.status_changed only.
        source:
          type: string
          enum:
            - pharmacy
            - system
          description: "Who triggered the change: a pharmacy user or key, or the system (payment, pharmacy-system sync)."
    OrderStatusBody:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - in_fulfillment
            - shipped
            - delivered
            - action_needed
    PingData:
      type: object
      required:
        - message
        - endpoint
      description: "ping: test event."
      properties:
        message:
          type: string
        endpoint:
          type: string
          pattern: "^whe_[0-9a-f-]{36}$"
          description: Object id with the whe_ prefix.
          example: whe_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
    PrescriptionRoutedData:
      type: object
      required:
        - prescription
      description: "prescription.routed (pharmacy only): a signed prescription was routed to the pharmacy, before payment."
      properties:
        prescription:
          type: object
          required:
            - id
            - object
            - patient
            - compound
            - strength
            - route
            - quantity
            - refills
            - days_supply
            - dea_schedule
            - provider
            - pharmacy
            - signed_at
          properties:
            id:
              type: string
              pattern: "^rx_[0-9a-f-]{36}$"
              description: Object id with the rx_ prefix.
              example: rx_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
            object:
              type: string
              const: prescription
            patient:
              type: string
              pattern: "^pt_[0-9a-f-]{36}$"
              description: Object id with the pt_ prefix.
              example: pt_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
            compound:
              type: string
              pattern: "^cmp_[0-9a-f-]{36}$"
              description: Object id with the cmp_ prefix.
              example: cmp_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
            strength:
              type: object
              properties:
                value:
                  type:
                    - string
                    - "null"
                unit:
                  type:
                    - string
                    - "null"
            route:
              type:
                - string
                - "null"
            quantity:
              type:
                - integer
                - "null"
            refills:
              type:
                - integer
                - "null"
            days_supply:
              type:
                - integer
                - "null"
            dea_schedule:
              type:
                - integer
                - "null"
              description: null means non-controlled.
            provider:
              type: string
              pattern: "^org_[0-9a-f-]{36}$"
              description: Object id with the org_ prefix.
              example: org_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
            pharmacy:
              type: string
              pattern: "^org_[0-9a-f-]{36}$"
              description: Object id with the org_ prefix.
              example: org_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
            signed_at:
              type: string
              format: date-time
    Product:
      type: object
      required:
        - id
        - object
        - compound
        - name
        - channel
        - status
        - published
        - delivery_method
        - strength
        - dosage_form_code
        - quantity_uom_code
        - pricing
        - latest_batch
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^prd_[0-9a-f-]{36}$"
          description: Object id with the prd_ prefix.
          example: prd_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: product
        compound:
          type: string
          pattern: "^cmp_[0-9a-f-]{36}$"
          description: Object id with the cmp_ prefix.
          example: cmp_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        name:
          type:
            - string
            - "null"
          description: Compound display name.
        channel:
          type: string
          enum:
            - patient_rx
            - office_stock
          description: patient_rx is the 503A channel, office_stock the 503B channel.
        status:
          type: string
          enum:
            - live
            - pending_review
            - draft
            - unlisted
            - hidden
          description: "draft: saved, not submitted. pending_review: awaiting PEPTPlus verification. live: approved and visible. unlisted: approved, hidden by the pharmacy. hidden: hidden by PEPTPlus."
        published:
          type: boolean
          description: Whether the pharmacy wants the product visible once approved.
        delivery_method:
          type: string
          enum:
            - subq
            - im
            - iv
            - topical
            - edible
        strength:
          oneOf:
            - "$ref": "#/components/schemas/Strength"
            - type: "null"
        dosage_form_code:
          type:
            - string
            - "null"
          description: NCPDP dosage form code.
        quantity_uom_code:
          type:
            - string
            - "null"
          description: NCPDP dispensing unit code.
        pricing:
          oneOf:
            - title: PatientRxPricing
              type: object
              required:
                - patient_price
                - price_basis
                - currency
              properties:
                patient_price:
                  type:
                    - string
                    - "null"
                  description: Dollars.
                price_basis:
                  type: string
                  enum:
                    - per_rx
                    - per_unit
                currency:
                  type: string
                  const: usd
            - title: OfficeStockPricing
              type: object
              required:
                - wholesale_price
                - pack_size
                - lead_time_days
                - currency
              properties:
                wholesale_price:
                  type:
                    - string
                    - "null"
                  description: Dollars.
                pack_size:
                  type:
                    - integer
                    - "null"
                lead_time_days:
                  type:
                    - integer
                    - "null"
                currency:
                  type: string
                  const: usd
          description: Shape depends on channel.
        latest_batch:
          oneOf:
            - type: object
              required:
                - id
                - lot_no
                - quantity
                - expiry_date
                - coa_status
              properties:
                id:
                  type: string
                  pattern: "^bat_[0-9a-f-]{36}$"
                  description: Object id with the bat_ prefix.
                  example: bat_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
                lot_no:
                  type:
                    - string
                    - "null"
                quantity:
                  type:
                    - integer
                    - "null"
                expiry_date:
                  type:
                    - string
                    - "null"
                  description: YYYY-MM-DD.
                coa_status:
                  type:
                    - string
                    - "null"
            - type: "null"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ProductCreate:
      type: object
      required:
        - compound
        - delivery_method
        - strength
        - dosage_form_code
        - quantity_uom_code
      properties:
        compound:
          type: string
          description: cmp_ id from GET /catalog (or the bare UUID).
        channel:
          type: string
          enum:
            - patient_rx
            - office_stock
          default: patient_rx
        delivery_method:
          type: string
          enum:
            - subq
            - im
            - iv
            - topical
            - edible
        strength:
          type: object
          required:
            - value
            - unit
          properties:
            value:
              oneOf:
                - type: number
                  exclusiveMinimum: 0
                - type: string
                  pattern: "^\\d+(\\.\\d+)?$"
            unit:
              type: string
              enum:
                - mg
                - mcg
                - g
                - IU
                - mL
                - mg/mL
                - mcg/mL
                - IU/mL
                - mEq
                - "%"
        dosage_form_code:
          type: string
          description: NCPDP dosage form code, 1 to 16 characters.
          minLength: 1
          maxLength: 16
        quantity_uom_code:
          type: string
          description: NCPDP dispensing unit code, 1 to 16 characters.
          minLength: 1
          maxLength: 16
        patient_price:
          oneOf:
            - type: number
              minimum: 0
            - type: string
              pattern: "^\\d+(\\.\\d{1,2})?$"
          description: Required for patient_rx unless published is false (draft).
        price_basis:
          type: string
          enum:
            - per_rx
            - per_unit
        pack_size:
          type: integer
          description: Required for office_stock.
          minimum: 1
        wholesale_price:
          oneOf:
            - type: number
              minimum: 0
            - type: string
              pattern: "^\\d+(\\.\\d{1,2})?$"
          description: Required for office_stock.
        lead_time_days:
          type: integer
          description: Required for office_stock.
          minimum: 0
        published:
          type: boolean
          description: "false saves a draft that is not submitted for review. Default: submit for review."
        batch:
          type: object
          required:
            - lot_no
          description: Optional first lot created with the product.
          properties:
            lot_no:
              type: string
              minLength: 1
              maxLength: 64
            quantity:
              type: integer
              minimum: 0
            expiry_date:
              type: string
              pattern: "^(\\d{1,2}/\\d{4}|\\d{4}-\\d{2}(-\\d{2})?)$"
              description: MM/YYYY, YYYY-MM or YYYY-MM-DD.
    ProductPatch:
      type: object
      minProperties: 1
      description: "At least one field. Setting published true shows an approved product in the marketplace; false unlists it. Changing delivery_method on an approved product sends it back for review."
      properties:
        published:
          type: boolean
        patient_price:
          oneOf:
            - type: number
              minimum: 0
            - type: string
              pattern: "^\\d+(\\.\\d{1,2})?$"
          description: Dollars as a number or a decimal string with up to two decimals. Stored and returned as a decimal string.
        price_basis:
          type: string
          enum:
            - per_rx
            - per_unit
        wholesale_price:
          oneOf:
            - type: number
              minimum: 0
            - type: string
              pattern: "^\\d+(\\.\\d{1,2})?$"
          description: Dollars as a number or a decimal string with up to two decimals. Stored and returned as a decimal string.
        pack_size:
          type: integer
          minimum: 1
        lead_time_days:
          type: integer
          minimum: 0
        delivery_method:
          type: string
          enum:
            - subq
            - im
            - iv
            - topical
            - edible
        dosage_form_code:
          type: string
          minLength: 1
          maxLength: 16
        quantity_uom_code:
          type: string
          minLength: 1
          maxLength: 16
    ProductPatched:
      allOf:
        - "$ref": "#/components/schemas/Product"
        - type: object
          required:
            - sent_to_review
          properties:
            sent_to_review:
              type: boolean
              description: True when the change put the product back into review.
    ShipmentBody:
      type: object
      required:
        - carrier
        - tracking_no
      properties:
        carrier:
          type: string
          description: Carrier name, 1 to 64 characters.
          minLength: 1
          maxLength: 64
        tracking_no:
          type: string
          description: Tracking number, 1 to 128 characters.
          minLength: 1
          maxLength: 128
    Strength:
      type: object
      required:
        - value
        - unit
      properties:
        value:
          type: string
          description: "Decimal string, for example \"5\"."
        unit:
          type:
            - string
            - "null"
          enum:
            - mg
            - mcg
            - g
            - IU
            - mL
            - mg/mL
            - mcg/mL
            - IU/mL
            - mEq
            - "%"
            - null
    WebhookDelivery:
      type: object
      required:
        - id
        - object
        - endpoint
        - event
        - type
        - status
        - attempt_count
        - next_attempt_at
        - last_attempt_at
        - last_status_code
        - last_error
        - delivered_at
        - replay_of
        - created_at
      properties:
        id:
          type: string
          pattern: "^whd_[0-9a-f-]{36}$"
          description: Object id with the whd_ prefix.
          example: whd_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: webhook_delivery
        endpoint:
          type: string
          pattern: "^whe_[0-9a-f-]{36}$"
          description: Object id with the whe_ prefix.
          example: whe_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        event:
          type: string
          pattern: "^evt_[0-9a-f-]{36}$"
          description: Object id with the evt_ prefix.
          example: evt_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        type:
          type: string
          enum:
            - order.created
            - order.accepted
            - order.status_changed
            - shipment.created
            - shipment.delivered
            - prescription.routed
            - batch.verified
            - listing.licensed
            - ping
        status:
          type: string
          enum:
            - pending
            - delivered
            - dead
        attempt_count:
          type: integer
        next_attempt_at:
          type:
            - string
            - "null"
          format: date-time
          description: Only while pending.
        last_attempt_at:
          type:
            - string
            - "null"
          format: date-time
        last_status_code:
          type:
            - integer
            - "null"
        last_error:
          type:
            - string
            - "null"
        delivered_at:
          type:
            - string
            - "null"
          format: date-time
        replay_of:
          type:
            - string
            - "null"
          pattern: "^whd_[0-9a-f-]{36}$"
          description: Object id with the whd_ prefix.
          example: whd_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        created_at:
          type: string
          format: date-time
    WebhookEndpoint:
      type: object
      required:
        - id
        - object
        - url
        - description
        - events
        - active
        - disabled_reason
        - secret_hint
        - stats
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^whe_[0-9a-f-]{36}$"
          description: Object id with the whe_ prefix.
          example: whe_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: webhook_endpoint
        url:
          type: string
        description:
          type:
            - string
            - "null"
        events:
          type: array
          items:
            type: string
          description: "Subscribed event types, or [\"*\"] for every event."
        active:
          type: boolean
        disabled_reason:
          type:
            - string
            - "null"
          description: Set when 50 consecutive failures disabled the endpoint.
        secret_hint:
          type: string
          description: Last characters of the signing secret, for identification.
        secret:
          type: string
          description: The signing secret (whsec_ plus 48 hex). Returned once, in the create response only.
        stats:
          type: object
          required:
            - delivered
            - failed
            - consecutive_failures
            - last_delivered_at
          properties:
            delivered:
              type: integer
            failed:
              type: integer
            consecutive_failures:
              type: integer
            last_delivered_at:
              type:
                - string
                - "null"
              format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    WebhookEndpointCreate:
      type: object
      required:
        - url
        - events
      properties:
        url:
          type: string
          description: https URL on a public host, 12 to 2048 characters. No credentials, loopback or private addresses.
          minLength: 12
          maxLength: 2048
        description:
          type:
            - string
            - "null"
          maxLength: 200
        events:
          type: array
          minItems: 1
          maxItems: 32
          items:
            type: string
            enum:
              - order.created
              - order.accepted
              - order.status_changed
              - shipment.created
              - shipment.delivered
              - prescription.routed
              - batch.verified
              - listing.licensed
              - "*"
          description: "Event types to receive. \"*\" subscribes to everything. Every endpoint receives ping."
    WebhookEndpointWithDeliveries:
      allOf:
        - "$ref": "#/components/schemas/WebhookEndpoint"
        - type: object
          required:
            - recent_deliveries
          properties:
            recent_deliveries:
              type: array
              items:
                "$ref": "#/components/schemas/WebhookDelivery"
              description: The 20 most recent deliveries, newest first.
    WebhookEvent:
      type: object
      required:
        - id
        - object
        - type
        - created
        - api_version
        - data
      description: "Body of every webhook delivery. `id` is the same for every endpoint and replay of one emission: deduplicate on it."
      properties:
        id:
          type: string
          pattern: "^evt_[0-9a-f-]{36}$"
          description: Object id with the evt_ prefix.
          example: evt_6c20a1f0-9b2e-4c1a-8e7d-3f5a2b1c0d9e
        object:
          type: string
          const: event
        type:
          type: string
          enum:
            - order.created
            - order.accepted
            - order.status_changed
            - shipment.created
            - shipment.delivered
            - prescription.routed
            - batch.verified
            - listing.licensed
            - ping
        created:
          type: integer
          description: Unix seconds when the event was emitted.
        api_version:
          type: string
          const: "2026-10-29"
        data:
          oneOf:
            - "$ref": "#/components/schemas/OrderEventData"
            - "$ref": "#/components/schemas/PrescriptionRoutedData"
            - "$ref": "#/components/schemas/BatchVerifiedData"
            - "$ref": "#/components/schemas/ListingLicensedData"
            - "$ref": "#/components/schemas/PingData"
