openapi: 3.1.0
info:
  title: Double Holo Seller API
  version: 1.0.0
  description: |
    Manage your Double Holo asks (listings), listing photos, sold orders, and shipping labels from your own software.

    Authentication. Every request sends `Authorization: Bearer <your key>` and `X-DH-User-Id: <your user id>`.
    Create keys in your seller settings on doubleholo.com (two active keys at most). A wrong key, a wrong user id,
    and a revoked key all answer 401 invalid_credentials. Twenty failed sign-ins from one network in 10 minutes
    block requests without a valid key from that network for an hour (429 rate_limited, Retry-After 3600).

    Access. The Seller API opens at seller level 2 for an approved seller who finished Stripe onboarding and phone
    verification and accepted the current Seller API terms. The Orders and Labels endpoints stay open to a seller
    whose approval, onboarding, or level lapsed, so sold orders can still be shipped; bans are never relaxed.
    503 seller_api_unavailable means the Seller API is not open for your account (or is paused). 503
    upstream_unavailable means our payment provider did not answer the seller check; retry after Retry-After (30
    seconds). 503 photo_processing_unavailable means photos cannot be processed right now; retry after Retry-After.

    Request bodies. Send JSON (`Content-Type: application/json`). Use multipart/form-data only to send photos
    (POST /asks with photos[], POST /asks/{ask_id}/photos). Form-urlencoded bodies are not accepted: the ask
    endpoints refuse them with 422 validation_failed (field Content-Type). A JSON body may be at most 1 MB and a
    photo upload at most 105 MB. A body larger than its limit, or a chunked body sent without a length, answers
    413 with the error code validation_failed before the key is checked. A request with no body needs no
    Content-Length. A field an endpoint does not take answers 422 unknown_parameter and lists it in `parameters`.

    Money is in whole US cents (`price_cents`, `shipping_cents`, `amount_cents`). A float (1250.5) or a JSON string
    ("1250") is refused with 422 validation_failed, never rounded. In a multipart form, where every value is text, a
    string of digits is read as cents.

    Languages in responses are lowercase keys: english, japanese, french, german, italian, spanish, korean,
    chinese_traditional, chinese_simplified, portuguese, russian. Cards, asks, and order items all use them, and the
    cards search `language` filter takes the same keys. When you create an ask, `language` takes one of these
    keys (english when omitted); any other value is refused with 422 validation_failed. In a multipart form, an
    empty `language` field counts as omitted (english).

    Lists. GET /asks and GET /orders return newest first with cursor pagination: `limit` from 1 to 100 (default
    50), then pass `next_cursor` as `cursor` until it is null. A cursor that is not one we returned answers 422
    (field cursor). `updated_since` is an ISO 8601 time from 2000-01-01T00:00:00Z to one day from now.

    Idempotency. Six calls require an Idempotency-Key (400 idempotency_key_required without it): POST /asks, POST
    /asks/bulk, POST /orders/{id}/cancel, POST /orders/{id}/refunds, POST /orders/{id}/shipping/label, and DELETE
    /orders/{id}/shipping/label. Six more accept one but do not need it: PATCH /asks/{id}, POST /asks/bulk-price,
    DELETE /asks/{id}, PUT /asks/{ask_id}/photos/order, DELETE /asks/{ask_id}/photos/{id}, and POST
    /orders/{id}/ship. Every other call, including the photo upload, does not take one and ignores the header.
    A key is 1 to 255 visible ASCII characters and is kept for 48 hours. Repeating a request with the same key replays the first answer with
    `Idempotent-Replayed: true` and uses no daily cap again. The same key on a different request answers 422
    idempotency_key_reused. While the first request is still running, the key answers 409 idempotency_in_progress.
    A 429 frees the key for a later retry.
    Money actions (cancel, refund, label purchase, label void) and ask creation (POST /asks, POST /asks/bulk) never
    run twice for one key. They answer 409 idempotency_in_progress until the first request settles, and a server
    error (5xx) is stored as the key's final answer. If one of these requests fails with a 5xx, or the connection
    drops before you get an answer, check the order with GET /orders/{id} (or your asks with GET /asks) before you
    try again, and use a new key: the old key keeps its stored answer, or stays in progress if the request never
    settled. On other writes, a request that failed with a 5xx can be retried with the same key after 2 minutes.

    Rate limits and daily caps depend on your seller level (see x-dh-limits). Responses to authenticated requests
    carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix seconds) for whichever window,
    minute or hour, has fewer requests left. The 429 for too many failed sign-ins and the 413 for a body that is
    too large are refused before the API reads the request, so they carry no rate limit headers. Going over a
    limit answers 429 rate_limited with Retry-After and `retry_after_seconds`.
    Daily caps reset at midnight UTC; going over answers 429 daily_limit_reached with `action`, `limit`, `used`,
    `remaining`, `resets_at`, and Retry-After. A request that is refused before anything changes gives its use back.
    `refund_amount` caps the refunded dollars of refunds and cancels together.

    Errors use `{ "error": { "code": "...", "message": "...", ...details } }`; `field` names the field at fault.
    The codes and their statuses are in x-dh-error-codes. Any other path answers 404 not_found. Every response is
    sent with `Cache-Control: no-store`.
servers:
  - url: https://api.doubleholo.com/api/v1/seller-api
tags:
  - name: Account
    description: Your account, limits, and today's usage.
  - name: Cards
    description: Find the card ids to list against.
  - name: Asks
    description: Your listings for sale.
  - name: Photos
    description: The photos on one of your asks.
  - name: Orders
    description: Orders you sold, and shipping, cancelling, or refunding them.
  - name: Labels
    description: Double Holo shipping labels for your sold orders.
security:
  - SellerKey: []
    SellerUserId: []
paths:
  /me:
    get:
      tags: [Account]
      summary: Your account, level, limits, and terms version
      operationId: getMe
      responses:
        '200':
          description: Your account.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Me' }
        default: { $ref: '#/components/responses/Error' }
  /usage:
    get:
      tags: [Account]
      summary: Today's usage against each daily cap
      operationId: getUsage
      responses:
        '200':
          description: Today's usage (UTC day).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Usage' }
        default: { $ref: '#/components/responses/Error' }
  /cards/search:
    get:
      tags: [Cards]
      summary: Find card ids to list against
      description: Searches cards you can list by name, optionally narrowed by set, number, and language. Sorted by name, then set.
      operationId: searchCards
      parameters:
        - { name: q, in: query, required: true, description: Part of the card name., schema: { type: string, minLength: 3, maxLength: 100 } }
        - { name: set, in: query, description: Part of the set name, or the exact set code., schema: { type: string, maxLength: 100 } }
        - { name: number, in: query, description: The exact card number., schema: { type: string } }
        - { name: language, in: query, schema: { $ref: '#/components/schemas/Language' } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 50, default: 20 } }
      responses:
        '200':
          description: Matching cards.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Card' } }
        default: { $ref: '#/components/responses/Error' }
  /asks:
    get:
      tags: [Asks]
      summary: List your asks
      description: Newest first. `status=active` may include asks past `expires_at` until the expiry job sweeps them; `expires_at` is the authority on time.
      operationId: listAsks
      parameters:
        - { name: status, in: query, schema: { $ref: '#/components/schemas/AskStatus' } }
        - { name: card_id, in: query, schema: { type: integer, minimum: 1 } }
        - { $ref: '#/components/parameters/UpdatedSince' }
        - { $ref: '#/components/parameters/Cursor' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200':
          description: One page of asks.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Ask' } }
                  next_cursor: { type: [string, 'null'] }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Asks]
      summary: Create an ask
      description: |
        JSON, or multipart/form-data with up to 10 photos as photos[] (JPEG, PNG, or WebP, up to 10 MB and 50
        megapixels each). Photos are required on asks of $250 and above
        unless your account has a photo waiver, so those asks must be sent as multipart. In a multipart form, an empty
        `language` field counts as omitted (english). Uses one ask_create and one photo_upload per photo. If the ask saved but its photos could not be attached,
        the answer is an error that carries `ask_id`: below $250 the ask stays live without photos; at $250 and above
        it is cancelled (`ask_cancelled: true`), or left live if it could not be cancelled (`ask_cancelled: false`).
        A price far from the market answers price_confirmation_required; resend with `price_confirmed: true`.
        Shipping is limited to half `price_cents`, or $10 when that is more; a higher `shipping_cents` answers 422
        validation_failed with field shipping_cents, before any photo is processed.
        A request the ask checks refuse is refused before any photo is processed, so it uses no photo_upload.
        A 503 photo_processing_unavailable (photo processing unavailable or busy) carries Retry-After.
      operationId: createAsk
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      x-dh-daily-caps: [ask_create, photo_upload]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AskCreate' }
          multipart/form-data:
            schema: { $ref: '#/components/schemas/AskCreateMultipart' }
      responses:
        '201':
          description: The new ask.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Ask' }
        default: { $ref: '#/components/responses/Error' }
  /asks/bulk:
    post:
      tags: [Asks]
      summary: Create up to 50 asks with a result per row
      description: |
        JSON only, no photos. A row priced at $250 or more needs photos (unless your account has a photo waiver), so
        it fails with validation_failed on field photos and cannot be created here; create it with POST /asks and
        photos instead. Rows that fail the field checks are reported without using a cap; ask_create is
        reserved for every other row up front, so a batch that would cross the daily cap is refused whole, and a
        row that fails later gives its use back. 201 when every row was created, 207 when some were, 422 when none
        were. Each failed row carries its own error code (for example seller_level_limit or
        price_confirmation_required). A row with `shipping_cents` above half its `price_cents` (or $10 when that is
        more) fails with validation_failed and field shipping_cents.
      operationId: bulkCreateAsks
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      x-dh-daily-caps: [ask_create]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [asks]
              properties:
                asks: { type: array, minItems: 1, maxItems: 50, items: { $ref: '#/components/schemas/AskCreate' } }
      responses:
        '201': { $ref: '#/components/responses/BulkCreate' }
        '207': { $ref: '#/components/responses/BulkCreate' }
        '422': { $ref: '#/components/responses/BulkCreate' }
        default: { $ref: '#/components/responses/Error' }
  /asks/bulk-price:
    post:
      tags: [Asks]
      summary: Reprice up to 100 asks
      description: |
        Uses one ask_update per row; a row that fails gives its use back. A lower price can leave shipping you set
        above half the new price (or $10 when that is more); that row fails with validation_failed and the message
        names the limit. Shipping that comes from your shipping tiers is lowered to the limit instead.
      operationId: bulkPriceAsks
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      x-dh-daily-caps: [ask_update]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [updates]
              properties:
                updates:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [id, price_cents]
                    properties:
                      id: { type: integer, minimum: 1 }
                      price_cents: { type: integer, minimum: 1, maximum: 9999999999 }
      responses:
        '200':
          description: The rows that were repriced and the rows that were not.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      updated:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: integer }
                            price_cents: { type: integer }
                            quantity: { type: integer }
                      failed:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: integer }
                            error: { $ref: '#/components/schemas/ErrorBody' }
        default: { $ref: '#/components/responses/Error' }
  /asks/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      tags: [Asks]
      summary: Get one ask
      operationId: getAsk
      responses:
        '200': { $ref: '#/components/responses/AskData' }
        default: { $ref: '#/components/responses/Error' }
    patch:
      tags: [Asks]
      summary: Edit price, shipping, quantity, description, or offers
      description: |
        Active and partially filled asks can be edited; a quantity cannot go below what has already sold. Editing an
        expired ask also relists it: the ask goes back on sale (status active, no expiry), and the full Seller API
        gate and seller phone verification apply (a seller without a verified phone gets 403 onboarding_required).
        Filled and cancelled asks answer 422 invalid_state. A null `shipping_cents` clears the ask's own shipping
        price. Shipping is limited to half the ask's price, or $10 when that is more: a changed `shipping_cents`
        above the limit, or a lower `price_cents` that leaves shipping you set above the new limit, answers 422
        validation_failed with field shipping_cents. Shipping that comes from your shipping tiers is lowered to the
        limit instead of refused. Resending the shipping the ask already has is not a change.
      operationId: updateAsk
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      x-dh-daily-caps: [ask_update]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AskUpdate' }
      responses:
        '200': { $ref: '#/components/responses/AskData' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [Asks]
      summary: Cancel an ask
      description: Set `remove_hub_item=true` to also remove the linked Vendor Hub inventory item.
      operationId: cancelAsk
      x-dh-daily-caps: [ask_update]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
        - { name: remove_hub_item, in: query, schema: { type: boolean, default: false } }
      responses:
        '200':
          description: The cancelled ask.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ask: { $ref: '#/components/schemas/Ask' }
                      hub_item_removed: { type: [boolean, 'null'] }
        default: { $ref: '#/components/responses/Error' }
  /asks/{ask_id}/photos:
    parameters:
      - $ref: '#/components/parameters/AskId'
    get:
      tags: [Photos]
      summary: List an ask's photos
      operationId: listAskPhotos
      responses:
        '200': { $ref: '#/components/responses/PhotoList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Photos]
      summary: Upload JPEG, PNG, or WebP photos up to 10 MB and 50 megapixels each
      description: Multipart form data with the files as photos[]. An ask holds at most 10 photos. Does not take an Idempotency-Key. Answers with every photo on the ask. A 503 photo_processing_unavailable (photo processing unavailable or busy) carries Retry-After.
      operationId: uploadAskPhotos
      x-dh-daily-caps: [photo_upload]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [photos]
              properties:
                photos: { type: array, minItems: 1, maxItems: 10, items: { type: string, format: binary } }
      responses:
        '201': { $ref: '#/components/responses/PhotoList' }
        default: { $ref: '#/components/responses/Error' }
  /asks/{ask_id}/photos/order:
    parameters:
      - $ref: '#/components/parameters/AskId'
    put:
      tags: [Photos]
      summary: Reorder an ask's photos
      description: List every photo id exactly once, in the new order. Photo ids change after a reorder, so read them from the response.
      operationId: reorderAskPhotos
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      x-dh-daily-caps: [ask_update]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [photo_ids]
              properties:
                photo_ids: { type: array, items: { type: integer } }
      responses:
        '200': { $ref: '#/components/responses/PhotoList' }
        default: { $ref: '#/components/responses/Error' }
  /asks/{ask_id}/photos/{id}:
    parameters:
      - $ref: '#/components/parameters/AskId'
      - $ref: '#/components/parameters/Id'
    delete:
      tags: [Photos]
      summary: Delete a photo
      description: |
        The last photo of an ask of $250 and above cannot be deleted, unless the ask is graded and has a cert number
        or cert images, or your account has a photo waiver. Asks under $250 can have every photo removed. Answers
        with the photos left.
      operationId: deleteAskPhoto
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      x-dh-daily-caps: [ask_update]
      responses:
        '200': { $ref: '#/components/responses/PhotoList' }
        default: { $ref: '#/components/responses/Error' }
  /orders:
    get:
      tags: [Orders]
      summary: List your sold orders
      description: Newest first. Only orders the buyer paid for. In this list `can_refund` is a quick check; GET /orders/{id} has the full answer.
      operationId: listOrders
      parameters:
        - { name: status, in: query, schema: { $ref: '#/components/schemas/OrderStatus' } }
        - { $ref: '#/components/parameters/UpdatedSince' }
        - { $ref: '#/components/parameters/Cursor' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200':
          description: One page of orders.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Order' } }
                  next_cursor: { type: [string, 'null'] }
        default: { $ref: '#/components/responses/Error' }
  /orders/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      tags: [Orders]
      summary: Get one sold order
      operationId: getOrder
      responses:
        '200': { $ref: '#/components/responses/OrderData' }
        default: { $ref: '#/components/responses/Error' }
  /orders/{id}/ship:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      tags: [Orders]
      summary: Mark shipped with tracking
      description: |
        Send a tracking_number, or `untracked: true` where the order allows it (not both). An order that must ship on
        a Double Holo label answers platform_label_required. The tracking number of a voided label is refused, and an
        order whose label was voided must ship with a tracking number (untracked answers invalid_state). While a
        label purchase for the order is in progress, the request answers invalid_state; try again in a few minutes.
        No daily cap.
      operationId: shipOrder
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tracking_number: { type: string }
                carrier: { type: string }
                tracking_url: { type: string }
                untracked: { type: boolean }
      responses:
        '200': { $ref: '#/components/responses/OrderData' }
        default: { $ref: '#/components/responses/Error' }
  /orders/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      tags: [Orders]
      summary: Cancel an unshipped order; the buyer refund follows
      description: |
        The buyer is refunded everything left on the order shortly after (`refund_pending`). Uses one order_cancel
        and that amount of refund_amount. An order whose refund alone is more than your whole daily refund_amount
        limit answers invalid_state (cancel it on doubleholo.com), as does an order a label is being bought for.
      operationId: cancelOrder
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      x-dh-daily-caps: [order_cancel, refund_amount]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason: { type: string, enum: [Listing error, Inventory Error, Item Damaged or Lost, Buyer request] }
      responses:
        '200':
          description: The cancelled order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      order: { $ref: '#/components/schemas/Order' }
                      refund_pending: { type: boolean }
                      refund_amount_cents: { type: integer }
        default: { $ref: '#/components/responses/Error' }
  /orders/{id}/refunds:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      tags: [Orders]
      summary: Refund all or part of an order
      description: |
        Omit amount_cents for a full refund of everything left. A partial refund may not be more than what is left
        (`remaining_refundable_cents` in the error). Orders past payout answer refund_not_allowed. Uses one refund
        and the amount of refund_amount. A refund (full or partial) larger than your whole daily refund_amount limit
        answers 422 validation_failed on field amount_cents with `limit`, not daily_limit_reached. Your
        Idempotency-Key also keys the payment provider refund, so a retry with the same key never refunds twice.
      operationId: refundOrder
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      x-dh-daily-caps: [refund, refund_amount]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                amount_cents: { type: integer, minimum: 1, description: Omit for a full refund. }
                reason: { type: string, maxLength: 500 }
      responses:
        '200':
          description: The refund and the order after it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      order: { $ref: '#/components/schemas/Order' }
                      refund:
                        type: object
                        properties:
                          amount_cents: { type: integer }
                          already_refunded: { type: boolean }
        default: { $ref: '#/components/responses/Error' }
  /orders/{id}/shipping/rates:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      tags: [Labels]
      summary: Quote shipping label rates
      description: |
        Choose insurance and signature here; the label you buy keeps the extras its rate was quoted with.
        `payout_cents` is your payout for the order before a label, and a rate with `exceeds_payout: true` cannot
        be bought from the API. No daily cap.
      operationId: quoteLabelRates
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                parcel:
                  type: object
                  description: Each side at most 108 inches, weight at most 150 lb. Units default to in and lb.
                  required: [length, width, height, weight]
                  properties:
                    length: { type: number, exclusiveMinimum: 0 }
                    width: { type: number, exclusiveMinimum: 0 }
                    height: { type: number, exclusiveMinimum: 0 }
                    weight: { type: number, exclusiveMinimum: 0 }
                    distance_unit: { type: string, enum: [cm, in, ft, mm, m, yd] }
                    mass_unit: { type: string, enum: [g, oz, lb, kg] }
                return_address_id: { type: integer, minimum: 1, description: One of your saved return addresses; your default when omitted. }
                include_insurance: { type: boolean }
                include_signature: { type: boolean }
      responses:
        '200':
          description: Rates for this order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Rates' }
        default: { $ref: '#/components/responses/Error' }
  /orders/{id}/shipping/label:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      tags: [Labels]
      summary: Buy a label; its cost comes out of this order's payout
      description: |
        Use a rate id from POST /orders/{id}/shipping/rates. Only a paid order with no active label. A label that
        costs more than the payout left for this order answers label_cost_exceeds_payout.
      operationId: buyLabel
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      x-dh-daily-caps: [label_purchase]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rate_id]
              properties:
                rate_id: { type: string, maxLength: 255 }
                label_format: { type: string, enum: [PDF, PDF_4x6, PDF_4x8, PNG, ZPL] }
      responses:
        '201':
          description: The label and the order after it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      order: { $ref: '#/components/schemas/Order' }
                      label:
                        type: object
                        properties:
                          label_url: { type: string }
                          tracking_number: { type: string }
                          tracking_url: { type: [string, 'null'] }
                          cost_cents: { type: integer, description: Deducted from your payout for this order. }
        default: { $ref: '#/components/responses/Error' }
    get:
      tags: [Labels]
      summary: Get the label and tracking
      description: |
        `label_url` is the carrier's link from purchase and may expire. An envelope label bought on the web through
        TCGTracking answers `provider: tcg_tracking` with `label_url` null and its tracking.
      operationId: getLabel
      responses:
        '200':
          description: The label.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      label_url: { type: [string, 'null'] }
                      tracking_number: { type: [string, 'null'] }
                      tracking_url: { type: [string, 'null'] }
                      provider: { type: string, enum: [shippo, tcg_tracking] }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [Labels]
      summary: Void the label; its cost is returned only if the carrier refund is accepted
      description: |
        Once per order, while it is shipped and not yet delivered or in a payout. Only labels bought on Double Holo
        through our carrier partner; envelope labels are reset on the web. `label_cost_kept` is true when the carrier
        did not accept the refund and the cost stays deducted.
      operationId: voidLabel
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      x-dh-daily-caps: [label_void]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, maxLength: 500 }
      responses:
        '200':
          description: The order after the void.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      order: { $ref: '#/components/schemas/Order' }
                      refund_status: { type: [string, 'null'] }
                      label_cost_kept: { type: boolean }
        default: { $ref: '#/components/responses/Error' }
components:
  securitySchemes:
    SellerKey:
      type: http
      scheme: bearer
      description: dh_sk_live_ followed by 40 lowercase hex characters.
    SellerUserId:
      type: apiKey
      in: header
      name: X-DH-User-Id
      description: Your Double Holo user id, shown in your seller settings next to your keys.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: 1 to 255 visible ASCII characters, unique per request you mean to make once.
      schema: { type: string, minLength: 1, maxLength: 255 }
    IdempotencyKeyOptional:
      name: Idempotency-Key
      in: header
      required: false
      description: Optional on this call. 1 to 255 visible ASCII characters, unique per request you mean to make once.
      schema: { type: string, minLength: 1, maxLength: 255 }
    Id: { name: id, in: path, required: true, schema: { type: integer, minimum: 1 } }
    AskId: { name: ask_id, in: path, required: true, schema: { type: integer, minimum: 1 } }
    Cursor: { name: cursor, in: query, description: next_cursor from the previous page., schema: { type: string } }
    Limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
    UpdatedSince:
      name: updated_since
      in: query
      description: Only rows changed at or after this ISO 8601 time, from 2000-01-01T00:00:00Z to one day from now.
      schema: { type: string, format: date-time }
  headers:
    CacheControl: { description: Always no-store., schema: { type: string } }
    RateLimitLimit: { description: Requests allowed in the window with fewer requests left (minute or hour)., schema: { type: integer } }
    RateLimitRemaining: { description: Requests left in that window., schema: { type: integer } }
    RateLimitReset: { description: When that window resets, in Unix seconds., schema: { type: integer } }
    RetryAfter: { description: Seconds to wait before retrying (429, and any 503 except seller_api_unavailable)., schema: { type: integer } }
    IdempotentReplayed: { description: true when the answer is a replay of an earlier request with the same Idempotency-Key., schema: { type: string } }
  responses:
    Error:
      description: An error. See x-dh-error-codes for every code and its status.
      headers:
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
        Retry-After: { $ref: '#/components/headers/RetryAfter' }
        X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error: { $ref: '#/components/schemas/ErrorBody' }
    AskData:
      description: The ask.
      content:
        application/json:
          schema:
            type: object
            properties:
              data: { $ref: '#/components/schemas/Ask' }
    PhotoList:
      description: Every photo on the ask, in order.
      content:
        application/json:
          schema:
            type: object
            properties:
              data: { type: array, items: { $ref: '#/components/schemas/Photo' } }
    OrderData:
      description: The order.
      content:
        application/json:
          schema:
            type: object
            properties:
              data: { $ref: '#/components/schemas/Order' }
    BulkCreate:
      description: The rows created and the rows that failed, each with its index in the request.
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: object
                properties:
                  created:
                    type: array
                    items:
                      type: object
                      properties:
                        index: { type: integer }
                        ask: { $ref: '#/components/schemas/Ask' }
                  failed:
                    type: array
                    items:
                      type: object
                      properties:
                        index: { type: integer }
                        error: { $ref: '#/components/schemas/ErrorBody' }
  schemas:
    ErrorBody:
      type: object
      required: [code, message]
      additionalProperties: true
      properties:
        code: { type: string, description: One of x-dh-error-codes. }
        message: { type: string }
        field: { type: string }
        parameters: { type: array, items: { type: string } }
    Language:
      type: string
      enum: [english, japanese, french, german, italian, spanish, korean, chinese_traditional, chinese_simplified, portuguese, russian]
    AskStatus:
      type: string
      enum: [active, filled, partially_filled, cancelled, expired]
    OrderStatus:
      type: string
      enum: [paid, shipped, delivered, completed, cancelled, disputed, refunded]
    Card:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        set_name: { type: [string, 'null'] }
        set_code: { type: [string, 'null'] }
        number: { type: [string, 'null'] }
        language: { type: [string, 'null'], description: A lowercase language key. }
        is_sealed: { type: boolean }
    Photo:
      type: object
      properties:
        id: { type: integer, description: Changes after a reorder. }
        url: { type: [string, 'null'] }
        thumbnail_url: { type: [string, 'null'] }
        position: { type: integer }
    Ask:
      type: object
      properties:
        id: { type: integer }
        card_id: { type: integer }
        card: { oneOf: [{ $ref: '#/components/schemas/Card' }, { type: 'null' }] }
        item_type: { type: string, enum: [card, sealed] }
        sealed_product_name: { type: [string, 'null'] }
        condition: { type: [string, 'null'], enum: [NM, LP, MP, HP, DMG, null] }
        grading_company: { type: [string, 'null'] }
        grade: { type: [number, 'null'] }
        cert_number: { type: [string, 'null'] }
        language: { type: [string, 'null'], description: A lowercase language key. }
        edition: { type: [string, 'null'] }
        price_cents: { type: integer }
        shipping_cents: { type: [integer, 'null'] }
        flat_shipping: { type: [boolean, 'null'] }
        quantity: { type: integer }
        filled_quantity: { type: integer }
        remaining_quantity: { type: integer }
        available_quantity: { type: integer, description: Units a buyer can take right now, less units held by accepted offers. 0 unless the ask is active or partially filled. }
        status: { $ref: '#/components/schemas/AskStatus' }
        accepts_offers: { type: [boolean, 'null'] }
        description: { type: [string, 'null'] }
        expires_at: { type: [string, 'null'], format: date-time, description: The ask stops selling at this time even if its status still reads active. }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        photos: { type: array, items: { $ref: '#/components/schemas/Photo' } }
    AskCreate:
      type: object
      required: [card_id, price_cents]
      properties:
        card_id: { type: integer, minimum: 1, description: A card id from GET /cards/search. }
        item_type: { type: string, enum: [card, sealed], default: card }
        sealed_product_name: { type: string }
        condition: { type: string, enum: [NM, LP, MP, HP, DMG], description: Raw cards only; a graded ask takes its condition from the grade. }
        grading_company: { type: string, enum: [PSA, BGS, CGC, TAG] }
        grade: { type: number }
        cert_number: { type: string }
        language: { type: string, enum: [english, japanese, french, german, italian, spanish, korean, chinese_traditional, chinese_simplified, portuguese, russian], default: english, description: "A lowercase language key, the same keys responses use (for example japanese or chinese_traditional). English when omitted. Any other value is refused with 422 validation_failed and field language." }
        edition: { type: string }
        price_cents: { type: integer, minimum: 1, maximum: 9999999999 }
        shipping_cents: { type: integer, minimum: 0, maximum: 9999999999, description: "Whole US cents. At most half of price_cents, or 1000 ($10) when that is more. A higher value is refused with 422 validation_failed and field shipping_cents. When you leave it out, shipping comes from your shipping tiers and is lowered to that limit if it is higher." }
        flat_shipping: { type: boolean }
        quantity: { type: integer, minimum: 1 }
        description: { type: string, maxLength: 100 }
        accepts_offers: { type: boolean }
        expires_in_hours: { type: integer, minimum: 1 }
        price_confirmed: { type: boolean, description: Send true to list at a price far from the market. }
    AskCreateMultipart:
      allOf:
        - $ref: '#/components/schemas/AskCreate'
        - type: object
          properties:
            photos:
              type: array
              maxItems: 10
              description: JPEG, PNG, or WebP, up to 10 MB and 50 megapixels each.
              items: { type: string, format: binary }
    AskUpdate:
      type: object
      properties:
        price_cents: { type: integer, minimum: 1, maximum: 9999999999 }
        shipping_cents: { type: [integer, 'null'], minimum: 0, maximum: 9999999999, description: "Whole US cents. At most half of the ask's price, or 1000 ($10) when that is more. A changed value above that is refused with 422 validation_failed and field shipping_cents, and so is a price_cents change that leaves shipping you set above the new limit. Shipping from your shipping tiers is lowered to the limit instead." }
        quantity: { type: integer, minimum: 1 }
        description: { type: string, maxLength: 100 }
        accepts_offers: { type: boolean }
        flat_shipping: { type: boolean }
    OrderItem:
      type: object
      description: One line of the order. A lot offer order has one item per card, each with quantity 1, ask_id null, and no language or edition.
      properties:
        ask_id: { type: [integer, 'null'] }
        card_id: { type: [integer, 'null'] }
        card_name: { type: [string, 'null'] }
        condition: { type: [string, 'null'] }
        grading_company: { type: [string, 'null'] }
        grade: { type: [number, 'null'] }
        language: { type: [string, 'null'], description: A lowercase language key. }
        edition: { type: [string, 'null'] }
        quantity: { type: integer }
        unit_price_cents: { type: [integer, 'null'], description: The price of one unit. }
    Order:
      type: object
      properties:
        id: { type: integer }
        order_number: { type: string }
        status: { $ref: '#/components/schemas/OrderStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        paid_at: { type: [string, 'null'], format: date-time }
        shipped_at: { type: [string, 'null'], format: date-time }
        delivered_at: { type: [string, 'null'], format: date-time }
        cancelled_at: { type: [string, 'null'], format: date-time }
        items: { type: array, items: { $ref: '#/components/schemas/OrderItem' } }
        subtotal_cents: { type: integer }
        total_cents: { type: integer }
        seller_fee_cents: { type: integer }
        label_cost_cents: { type: integer, description: Everything deducted from your payout for labels, including voided labels the carrier did not refund. }
        refunded_cents: { type: integer }
        tracking_number: { type: [string, 'null'] }
        tracking_carrier: { type: [string, 'null'] }
        tracking_url: { type: [string, 'null'] }
        has_label: { type: boolean }
        label_voided_at: { type: [string, 'null'], format: date-time }
        shipping_requirements:
          type: object
          properties:
            tracking: { type: [boolean, 'null'] }
            signature: { type: [boolean, 'null'] }
            insurance: { type: [boolean, 'null'] }
            insurance_amount_cents: { type: [integer, 'null'] }
            hold_period_days: { type: [integer, 'null'] }
        requires_platform_label: { type: boolean }
        can_cancel: { type: boolean }
        can_refund: { type: boolean }
        buyer:
          description: The buyer's name and shipping address, only while the order is paid and waiting to ship.
          oneOf:
            - type: 'null'
            - type: object
              properties:
                name: { type: [string, 'null'] }
                address:
                  type: object
                  properties:
                    street1: { type: [string, 'null'] }
                    street2: { type: [string, 'null'] }
                    city: { type: [string, 'null'] }
                    state: { type: [string, 'null'] }
                    zip: { type: [string, 'null'] }
                    country: { type: string }
    Rates:
      type: object
      properties:
        shipment_id: { type: string }
        payout_cents: { type: [integer, 'null'], description: Your payout for this order before a label, less voided labels still deducted. }
        rates:
          type: array
          items:
            type: object
            properties:
              id: { type: string, description: The rate_id to buy with. }
              carrier: { type: string }
              service: { type: string }
              price_cents: { type: integer }
              label_cost_cents: { type: [integer, 'null'] }
              platform_fee_cents: { type: [integer, 'null'] }
              exceeds_payout: { type: boolean }
              estimated_days: { type: [integer, 'null'] }
              arrives_by: { type: [string, 'null'] }
        insurance:
          type: object
          properties:
            included: { type: boolean }
            required: { type: boolean }
            amount_cents: { type: [integer, 'null'] }
        signature:
          type: object
          properties:
            included: { type: boolean }
            required: { type: boolean }
    Me:
      type: object
      properties:
        user_id: { type: integer }
        seller_level: { type: integer }
        terms_version_accepted: { type: [integer, 'null'] }
        terms_version_current: { type: integer }
        limits:
          type: object
          properties:
            requests_per_minute: { type: integer }
            requests_per_hour: { type: integer }
            daily:
              type: object
              description: The count cap per action, plus refund_amount_cents (the refunded dollars cap, in cents).
              additionalProperties: { type: integer }
    Usage:
      type: object
      properties:
        date: { type: string, format: date }
        resets_at: { type: string, format: date-time }
        actions:
          type: array
          description: One row per capped action. The refund row also carries the refunded dollars (refunds and cancels) in cents.
          items:
            type: object
            properties:
              action: { type: string, enum: [ask_create, ask_update, photo_upload, label_purchase, label_void, order_cancel, refund] }
              used: { type: integer }
              limit: { type: integer }
              amount_used_cents: { type: [integer, 'null'] }
              amount_limit_cents: { type: [integer, 'null'] }
x-dh-limits:
  levels:
    - { level: 2, name: Level 2 }
    - { level: 3, name: Expedited }
    - { level: 4, name: Gold }
  rate: { requests_per_minute: [30, 120, 300], requests_per_hour: [600, 4000, 10000] }
  daily:
    ask_create: [100, 1000, 5000]
    ask_update: [500, 5000, 20000]
    photo_upload: [200, 2000, 10000]
    label_purchase: [25, 200, 1000]
    label_void: [5, 10, 30]
    order_cancel: [3, 15, 50]
    refund: [3, 15, 50]
    refund_amount: [100000, 300000, 2500000]
x-dh-error-codes:
  - { code: authentication_required, status: 401, description: "The Authorization or X-DH-User-Id header is missing or malformed." }
  - { code: invalid_credentials, status: 401, description: "The key or user id is not valid, or the key was revoked." }
  - { code: seller_api_unavailable, status: 503, description: "The Seller API is not open for this account yet." }
  - { code: upstream_unavailable, status: 503, description: "We could not check your seller account with our payment provider. Retry after the Retry-After time." }
  - { code: photo_processing_unavailable, status: 503, description: "Photo processing is unavailable or busy right now. Retry after the Retry-After time." }
  - { code: account_restricted, status: 403, description: "The account is banned or cannot sell, and its keys were revoked." }
  - { code: onboarding_required, status: 403, description: "Seller setup is not finished; missing lists what is left." }
  - { code: level_required, status: 403, description: "The Seller API opens at seller level 2." }
  - { code: terms_required, status: 403, description: "Accept the current Seller API terms in your seller settings." }
  - { code: rate_limited, status: 429, description: "Too many requests in this minute or hour; wait for the reset." }
  - { code: daily_limit_reached, status: 429, description: "You reached today's cap for this action; it resets at midnight UTC." }
  - { code: not_found, status: 404, description: "The ask, order, photo, label, or key is not yours or does not exist." }
  - { code: validation_failed, status: 422, description: "A field is missing or not valid; field names it." }
  - { code: unknown_parameter, status: 422, description: "The request sent a field this endpoint does not take." }
  - { code: idempotency_key_required, status: 400, description: "This endpoint needs an Idempotency-Key header." }
  - { code: idempotency_key_reused, status: 422, description: "This Idempotency-Key was used for a different request." }
  - { code: idempotency_in_progress, status: 409, description: "A request with this Idempotency-Key is still running." }
  - { code: seller_level_limit, status: 422, description: "Your seller level does not allow this many listings, this much listed value, or this price." }
  - { code: invalid_state, status: 422, description: "The ask or order is not in a state that allows this action." }
  - { code: payment_failed, status: 402, description: "The payment provider could not complete the refund." }
  - { code: price_confirmation_required, status: 422, description: "The price looks far from the market; resend with price_confirmed true to list anyway." }
  - { code: duplicate_refund, status: 409, description: "A refund of this amount was just issued on this order." }
  - { code: refund_not_allowed, status: 422, description: "This order cannot be refunded from the API, for example after payout." }
  - { code: rate_not_for_order, status: 422, description: "The rate was not quoted for this order; quote rates again." }
  - { code: platform_label_required, status: 422, description: "This order must ship on a Double Holo label." }
  - { code: label_already_purchased, status: 409, description: "This order already has a label, or one is being bought." }
  - { code: label_cost_exceeds_payout, status: 422, description: "This label costs more than your payout for this order; choose a cheaper rate or ship it yourself." }
  - { code: max_active_keys, status: 422, description: "You already have two active keys; revoke one to create another." }
  - { code: terms_version_mismatch, status: 422, description: "These are not the current Seller API terms; reload and accept the current version." }
  - { code: internal_error, status: 500, description: "Something went wrong on our side; retry later." }
