{
  "openapi": "3.1.0",
  "info": {
    "title": "Packflip Partner API",
    "version": "2026-09-12",
    "description": "Build Packflip card packs into your product. Every request is scoped to the organization that owns the API key.\n\nGuides, including a sandbox quickstart, are at https://partner.packflip.xyz/docs, and an agent-readable index is at https://partner.packflip.xyz/llms.txt.\n\n## Authentication\n\nCreate an API key in the Partner Console and send it on every `/api/v2` request:\n\n```\nAuthorization: Bearer pk_...\n```\n\nKeys act for the whole organization. Keep them on your server.\n\n## Card lifecycle\n\n1. **Draw** with `kind: \"order\"`. Cards are held by Packflip for the customer. Choose `reveal`:\n   - `on_create` (default): cards are revealed at once and cannot be refunded.\n   - `sealed`: cards stay hidden and can be refunded with `kind: \"refund\"` until revealed (`POST /customers/{customerId}/cards/reveal`) or until `refundableUntil`, after which they count as revealed.\n2. **Use** a revealed, held card: buy it back (`kind: \"buyback\", mode: \"offchain\"`), redeem it (`kind: \"redemption\"`), or withdraw it as an NFT (`kind: \"mint\"`).\n3. **Mint** returns signed calldata per card. Submit it from the customer's wallet; confirmation arrives through the chain webhook, `refresh`, or an optional hash report. An expired signature can be re-signed (same token) or cancelled (the card stays held).\n4. A **minted** card is bought back with `kind: \"buyback\", mode: \"onchain\"`, which returns handler burn calldata.\n\n## Environments\n\n| Environment | Base URL | Network |\n| --- | --- | --- |\n| Production | `https://partner.packflip.xyz` | Base |\n| Sandbox | `https://sandbox.partner.packflip.xyz` | Base Sepolia |\n\nThe sandbox uses a separate database and a synthetic catalog, with the same console login.\n\n## Money\n\nAll USD amounts are decimal strings such as `\"25.000000\"`, never JSON floats. Your balance is prepaid: fund it with USDC, then operations draw from it.\n\n## Idempotency\n\nCommerce writes require an `Idempotency-Key` header. Replaying an identical request returns the original result with `replayed: true`. Reusing a key with different request data returns `409`.\n\n## Errors\n\nErrors share one shape, so you can branch on `error.code`:\n\n```json\n{ \"error\": { \"code\": \"insufficient_balance\", \"message\": \"...\" } }\n```\n\n| Code | Status |\n| --- | --- |\n| `invalid_request` | 400 |\n| `unauthorized` | 401 |\n| `forbidden` | 403 |\n| `not_found` | 404 |\n| `conflict`, `idempotency_conflict`, `insufficient_balance`, `out_of_stock` | 409 |\n| `service_unavailable` | 503 |\n\n## Webhook signatures\n\nEach delivery is a `POST` with these headers:\n\n- `webhook-id`: the event ID\n- `webhook-timestamp`: Unix seconds\n- `webhook-signature`: `v1,` followed by the hex HMAC-SHA256 of `{timestamp}.{raw body}`, keyed with the endpoint signing secret\n\nVerify the signature against the raw body before parsing it.\n"
  },
  "servers": [
    {
      "url": "https://partner.packflip.xyz",
      "description": "Production"
    },
    {
      "url": "https://sandbox.partner.packflip.xyz",
      "description": "Partner sandbox"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "tags": [
    { "name": "Account", "description": "Your organization, spendable balance, and the immutable ledger behind it." },
    { "name": "Funding", "description": "Top up your balance with USDC on Base. Deposits are credited after the transfer is verified on-chain and final." },
    { "name": "Catalog", "description": "The packs you can sell." },
    { "name": "Customers", "description": "Your end users. Customers own the cards that operations deliver." },
    { "name": "Operations", "description": "Orders, buybacks, and redemptions. Every operation settles against your prepaid balance." },
    { "name": "Webhooks", "description": "Signed event delivery for top-ups, operations, and shipments." },
    { "name": "System", "description": "Service status." }
  ],
  "x-tagGroups": [
    { "name": "Basics", "tags": ["Account", "Funding"] },
    { "name": "Commerce", "tags": ["Catalog", "Customers", "Operations"] },
    { "name": "Integration", "tags": ["Webhooks", "System"] }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "tags": ["System"],
        "summary": "Health check",
        "description": "ALB health check.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok"],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/account": {
      "get": {
        "tags": ["Account"],
        "summary": "Get account",
        "description": "Get the authenticated Partner account.",
        "responses": {
          "200": {
            "description": "Account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v2/balance": {
      "get": {
        "tags": ["Account"],
        "summary": "Get balance",
        "description": "Get spendable balance.",
        "responses": {
          "200": {
            "description": "Cash, bonus, and total spendable balance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v2/funding/receiving-address": {
      "get": {
        "tags": ["Funding"],
        "summary": "Get deposit address",
        "description": "Returns the account's environment-specific Base USDC receiving address. The address is dedicated to this account and never changes. Every confirmed USDC transfer into it is credited, with no minimum amount. The token and chain are fixed by Packflip policy; clients cannot choose them.",
        "responses": {
          "200": {
            "description": "Receiving wallet",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["receivingWallet"],
                  "properties": {
                    "receivingWallet": { "$ref": "#/components/schemas/ReceivingWallet" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/api/v2/funding/refresh": {
      "post": {
        "tags": ["Funding"],
        "summary": "Check for deposits",
        "description": "Scans the receiving address now and credits confirmed deposits. Deposits are normally credited from the chain webhook; use this when one seems missing.",
        "responses": {
          "200": {
            "description": "Scan result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["transfers", "detected", "confirmed"],
                  "properties": {
                    "transfers": { "type": "integer" },
                    "detected": { "type": "integer" },
                    "confirmed": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/api/v2/sandbox/credits": {
      "post": {
        "tags": ["Funding"],
        "summary": "Add sandbox test credit",
        "description": "Sandbox only. Adds up to $100 of cash balance without an on-chain deposit, once an hour per account, and only while the balance is below $1,000. Returns 409 with Retry-After when the hourly limit applies, and 404 in production.",
        "responses": {
          "201": {
            "description": "Credit added",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxCredit" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/api/v2/funding-intents": {
      "get": {
        "tags": ["Funding"],
        "summary": "List funding intents",
        "description": "List funding intents.",
        "parameters": [{ "$ref": "#/components/parameters/Limit" }],
        "responses": {
          "200": {
            "description": "Funding intents in reverse chronological order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["fundingIntents"],
                  "properties": {
                    "fundingIntents": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/FundingIntent" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "tags": ["Funding"],
        "summary": "Create funding intent",
        "description": "Creates a quote for the fixed environment chain, official Circle USDC contract, and the Partner's receiving wallet. An intent is not spendable credit; receipt verification and finality are required.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/FundingIntentRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Funding intent",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/FundingIntent" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/api/v2/funding-transactions": {
      "get": {
        "tags": ["Funding"],
        "summary": "List deposits",
        "description": "List observed Base USDC funding transactions.",
        "parameters": [{ "$ref": "#/components/parameters/Limit" }],
        "responses": {
          "200": {
            "description": "Funding transaction status in reverse chronological order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["fundingTransactions"],
                  "properties": {
                    "fundingTransactions": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/FundingTransaction" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/v2/ledger-entries": {
      "get": {
        "tags": ["Account"],
        "summary": "List ledger entries",
        "description": "List recent immutable ledger entries.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Ledger entries in reverse chronological order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LedgerEntry"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v2/shipping-countries": {
      "get": {
        "tags": ["Operations"],
        "summary": "List shipping countries",
        "description": "Countries a redemption can ship to, with the fee charged to the Partner balance.",
        "responses": {
          "200": {
            "description": "Supported shipping countries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": ["country", "feeUsd"],
                        "properties": {
                          "country": { "type": "string", "description": "English country name. Send it verbatim as shipmentInfo.country." },
                          "feeUsd": { "$ref": "#/components/schemas/Decimal" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/v2/vending-machines": {
      "get": {
        "tags": ["Catalog"],
        "summary": "List vending machines",
        "description": "List public, active vending machines.",
        "responses": {
          "200": {
            "description": "Partner catalog",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/VendingMachine"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v2/customers": {
      "get": {
        "tags": ["Customers"],
        "summary": "List customers",
        "description": "List customers owned by the authenticated Partner.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Customers in reverse chronological order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Customer"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": ["Customers"],
        "summary": "Create customer",
        "description": "A customer is your end user as seen by Packflip: a pseudonymous identity that cards and operations attach to, with optional analytics attributes. externalUserId is optional; once set, it cannot be rebound to a different customer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCustomerRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Customer created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/v2/customers/{customerId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerId"
        }
      ],
      "get": {
        "tags": ["Customers"],
        "summary": "Get customer",
        "description": "Get one customer.",
        "responses": {
          "200": {
            "description": "Customer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": ["Customers"],
        "summary": "Update customer",
        "description": "Set a missing externalUserId, or merge analytics attributes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCustomerRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated customer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/v2/customers/by-external-id/{externalUserId}": {
      "parameters": [
        {
          "name": "externalUserId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": ["Customers"],
        "summary": "Find customer by external ID",
        "description": "Resolve an external user identifier to a customer.",
        "responses": {
          "200": {
            "description": "Customer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v2/customers/{customerId}/cards": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerId"
        },
        {
          "$ref": "#/components/parameters/Limit"
        }
      ],
      "get": {
        "tags": ["Customers"],
        "summary": "List customer cards",
        "description": "List a customer's cards, newest first. buybackUsd is the current market buyback value, not a stored quote.",
        "responses": {
          "200": {
            "description": "Cards",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Card"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v2/customers/{customerId}/cards/reveal": {
      "parameters": [{ "$ref": "#/components/parameters/CustomerId" }],
      "post": {
        "tags": ["Customers"],
        "summary": "Reveal sealed cards",
        "description": "Reveals sealed cards. Revealing is irreversible: a revealed card can no longer be refunded.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": ["cardIds"],
                "properties": {
                  "cardIds": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10,
                    "uniqueItems": true,
                    "items": { "type": "string", "pattern": "^card_" }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Revealed cards",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Card" } }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/api/v2/operations": {
      "post": {
        "tags": ["Operations"],
        "summary": "Create operation",
        "description": "Draws cards (order), withdraws them as NFTs (mint), refunds sealed cards (refund), buys cards back (buyback, offchain for held cards, onchain for minted cards), or redeems held cards (redemption). Idempotency-Key is required. Replaying an identical request returns its existing operation with replayed=true; reusing the key for different request data returns 409.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/OffchainOrderRequest" },
                  { "$ref": "#/components/schemas/OffchainBuybackRequest" },
                  { "$ref": "#/components/schemas/OnchainBuybackRequest" },
                  { "$ref": "#/components/schemas/OffchainRedemptionRequest" },
                  { "$ref": "#/components/schemas/MintRequest" },
                  { "$ref": "#/components/schemas/RefundRequest" }
                ]
              },
              "examples": {
                "order": {
                  "summary": "Order, revealed at once",
                  "value": {
                    "kind": "order",
                    "mode": "offchain",
                    "customerId": "cus_8f2k1",
                    "vendingMachineId": 1,
                    "quantity": 1
                  }
                },
                "orderSealed": {
                  "summary": "Order, sealed",
                  "value": {
                    "kind": "order",
                    "mode": "offchain",
                    "customerId": "cus_8f2k1",
                    "vendingMachineId": 1,
                    "quantity": 3,
                    "reveal": "sealed",
                    "metadata": {
                      "purchaseId": "p_981"
                    }
                  }
                },
                "refund": {
                  "summary": "Refund sealed cards",
                  "value": {
                    "kind": "refund",
                    "customerId": "cus_8f2k1",
                    "cardIds": [
                      "card_3h7d0"
                    ]
                  }
                },
                "offchainBuyback": {
                  "summary": "Off-chain buyback",
                  "value": {
                    "kind": "buyback",
                    "mode": "offchain",
                    "customerId": "cus_8f2k1",
                    "cardIds": [
                      "card_3h7d0"
                    ]
                  }
                },
                "onchainBuyback": {
                  "summary": "On-chain buyback",
                  "value": {
                    "kind": "buyback",
                    "mode": "onchain",
                    "customerId": "cus_8f2k1",
                    "cardIds": [
                      "card_3h7d0"
                    ]
                  }
                },
                "redemption": {
                  "summary": "Redemption with emails",
                  "value": {
                    "kind": "redemption",
                    "mode": "offchain",
                    "customerId": "cus_8f2k1",
                    "cardIds": [
                      "card_3h7d0"
                    ],
                    "shipmentInfo": {
                      "country": "Japan",
                      "name": "Hanako Yamada",
                      "phone": "+81 90 1234 5678",
                      "address1": "1-2-3 Jingumae",
                      "address2": "Room 402",
                      "city": "Shibuya-ku",
                      "state": "Tokyo",
                      "postalCode": "150-0001"
                    },
                    "notifications": {
                      "channels": [
                        "customer_email",
                        "partner_email"
                      ],
                      "customerEmail": "hanako@example.com"
                    }
                  }
                },
                "mint": {
                  "summary": "Mint to a wallet",
                  "value": {
                    "kind": "mint",
                    "customerId": "cus_8f2k1",
                    "cardIds": [
                      "card_3h7d0"
                    ],
                    "walletAddress": "0x1111111111111111111111111111111111111111"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Operation created",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/OperationResponse" },
                "examples": {
                  "order": {
                    "summary": "A completed order (fields abbreviated in cards)",
                    "value": {
                      "id": "op_5t1q9",
                      "kind": "order",
                      "mode": "offchain",
                      "status": "completed",
                      "customerId": "cus_8f2k1",
                      "vendingMachineId": 1,
                      "quantity": 1,
                      "chargedUsd": "25.000000",
                      "creditedUsd": "0.000000",
                      "metadata": null,
                      "completedAt": "2026-09-18T02:00:00.000Z",
                      "createdAt": "2026-09-18T02:00:00.000Z",
                      "updatedAt": "2026-09-18T02:00:00.000Z",
                      "shipment": null,
                      "cards": [
                        {
                          "id": "card_3h7d0",
                          "status": "active",
                          "sealed": false,
                          "refundableUntil": null,
                          "name": "Charizard PSA 9",
                          "image": "https://…/card.webp",
                          "imageSrcset": "https://… 256w, https://… 512w, https://… 1024w",
                          "buybackUsd": "18.500000",
                          "onchain": null,
                          "createdAt": "2026-09-18T02:00:00.000Z"
                        }
                      ],
                      "affectedCards": [],
                      "onchainOrders": [],
                      "onchainBurns": [],
                      "replayed": false
                    }
                  }
                }
              }
            }
          },
          "200": {
            "description": "Idempotent replay",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/OperationResponse" } }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/api/v2/operations/{operationId}": {
      "parameters": [
        {
          "name": "operationId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^op_"
          }
        }
      ],
      "get": {
        "tags": ["Operations"],
        "summary": "Get operation",
        "description": "Get an operation owned by the authenticated Partner.",
        "responses": {
          "200": {
            "description": "Operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Operation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v2/operations/{operationId}/onchain-orders/{onchainOrderId}/submit": {
      "parameters": [
        {
          "name": "operationId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "pattern": "^op_" }
        },
        {
          "name": "onchainOrderId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "pattern": "^oco_" }
        }
      ],
      "post": {
        "tags": ["Operations"],
        "summary": "Report mint transaction",
        "description": "Optional early report of a mint transaction; the chain webhook also detects it. The card is marked minted only after a successful transaction that matches the signed calldata or mints this token to the wallet. A cancelled mint returns 409.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/OnchainTransactionSubmissionRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transaction submission recorded",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Operation" } } }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/api/v2/operations/{operationId}/onchain-burns/{onchainBurnId}/submit": {
      "parameters": [
        {
          "name": "operationId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "pattern": "^op_" }
        },
        {
          "name": "onchainBurnId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "pattern": "^ocb_" }
        }
      ],
      "post": {
        "tags": ["Operations"],
        "summary": "Report burn transaction",
        "description": "Optional early report of a buyback burn transaction; the chain webhook also detects it. The credit is posted when the transaction emits this burn's Burned event.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/OnchainTransactionSubmissionRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Operation",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Operation" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "400": { "$ref": "#/components/responses/InvalidRequest" }
        }
      }
    },
    "/api/v2/operations/{operationId}/refresh": {
      "parameters": [
        {
          "name": "operationId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "pattern": "^op_" }
        }
      ],
      "post": {
        "tags": ["Operations"],
        "summary": "Refresh on-chain status",
        "description": "Checks the chain now for this operation's pending mints and burns and confirms any that landed. Use it when a webhook may have been missed.",
        "responses": {
          "200": {
            "description": "Operation",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Operation" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/api/v2/operations/{operationId}/resign": {
      "parameters": [
        {
          "name": "operationId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "pattern": "^op_" }
        }
      ],
      "post": {
        "tags": ["Operations"],
        "summary": "Re-sign expired authorizations",
        "description": "Issues new signatures for pending mints and burns whose deadline passed without landing. Token IDs, burn request IDs, and cards stay the same. Returns 409 when nothing has expired.",
        "responses": {
          "200": {
            "description": "Operation",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Operation" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/api/v2/operations/{operationId}/cancel": {
      "parameters": [
        {
          "name": "operationId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "pattern": "^op_" }
        }
      ],
      "post": {
        "tags": ["Operations"],
        "summary": "Cancel expired authorizations",
        "description": "Cancels a mint (cards stay held) or an on-chain buyback (cards stay minted) once every pending signature has expired without landing. Returns 409 while a signature is still valid.",
        "responses": {
          "200": {
            "description": "Operation",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Operation" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/api/v2/operations/{operationId}/shipment": {
      "parameters": [
        {
          "name": "operationId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^op_"
          }
        }
      ],
      "put": {
        "tags": ["Operations"],
        "summary": "Update shipment (sandbox)",
        "description": "Sandbox only: move the shipment for a completed off-chain redemption along to test `shipment.updated` handling. Omitted fields are unchanged and `null` clears a text field. A request that changes nothing emits no event. In production Packflip updates shipments and this returns 403.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipmentUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shipment updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Shipment"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v2/operations/{operationId}/shipment/address": {
      "parameters": [
        {
          "name": "operationId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^op_"
          }
        }
      ],
      "put": {
        "tags": ["Operations"],
        "summary": "Correct shipping address",
        "description": "Replaces shipmentInfo while the shipment is created or exception, typically after Packflip put it on hold. The country cannot change. The shipment returns to created, statusReason is cleared, addressUpdatedAt is set, and shipment.updated is sent.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": ["shipmentInfo"],
                "properties": {
                  "shipmentInfo": {
                    "type": "object",
                    "required": ["country"],
                    "properties": {
                      "country": { "type": "string", "minLength": 1, "maxLength": 100, "description": "Must resolve to the country the shipment was charged for." }
                    },
                    "additionalProperties": true,
                    "description": "The complete address; it replaces the stored one. Same fields as a redemption's shipmentInfo."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Address replaced",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Shipment" },
                    {
                      "type": "object",
                      "required": ["changedFields"],
                      "properties": {
                        "changedFields": { "type": "array", "items": { "type": "string" } }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/v2/webhooks": {
      "get": {
        "tags": ["Webhooks"],
        "summary": "List endpoints",
        "description": "List webhook endpoints owned by the authenticated Partner.",
        "responses": {
          "200": {
            "description": "Webhook endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/WebhookEndpoint" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "tags": ["Webhooks"],
        "summary": "Create endpoint",
        "description": "Create a webhook endpoint and reveal its signing secret once.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateWebhookRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook endpoint created",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WebhookEndpointWithSecret" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "503": { "description": "Webhook delivery is not configured" }
        }
      }
    },
    "/api/v2/webhooks/{webhookEndpointId}": {
      "parameters": [
        { "$ref": "#/components/parameters/WebhookEndpointId" }
      ],
      "get": {
        "tags": ["Webhooks"],
        "summary": "Get endpoint",
        "description": "Get a webhook endpoint owned by the authenticated Partner.",
        "responses": {
          "200": {
            "description": "Webhook endpoint",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WebhookEndpoint" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "tags": ["Webhooks"],
        "summary": "Update endpoint",
        "description": "Update a webhook endpoint or rotate its signing secret.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UpdateWebhookRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook endpoint updated",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WebhookEndpointWithSecret" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "503": { "description": "Webhook delivery is not configured" }
        }
      },
      "delete": {
        "tags": ["Webhooks"],
        "summary": "Deactivate endpoint",
        "description": "Deactivate a webhook endpoint.",
        "responses": {
          "204": { "description": "Webhook endpoint deactivated" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/v2/webhooks/{webhookEndpointId}/deliveries": {
      "parameters": [
        { "$ref": "#/components/parameters/WebhookEndpointId" },
        {
          "name": "limit",
          "in": "query",
          "required": false,
          "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
        }
      ],
      "get": {
        "tags": ["Webhooks"],
        "summary": "List deliveries",
        "description": "List delivery attempts without duplicating event payloads.",
        "responses": {
          "200": {
            "description": "Webhook delivery attempts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/WebhookDelivery" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/v2/webhooks/{webhookEndpointId}/replay": {
      "parameters": [
        { "$ref": "#/components/parameters/WebhookEndpointId" }
      ],
      "post": {
        "tags": ["Webhooks"],
        "summary": "Replay event",
        "description": "Requeue a retained event for an active webhook endpoint.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ReplayWebhookRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["queued", "eventId"],
                  "properties": {
                    "queued": { "type": "boolean" },
                    "eventId": { "type": "string", "pattern": "^evt_" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Packflip API key"
      }
    },
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      },
      "CustomerId": {
        "name": "customerId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^cus_"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        }
      },
      "WebhookEndpointId": {
        "name": "webhookEndpointId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^wh_"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InvalidRequest": {
        "description": "Malformed request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The operation is not allowed in this environment",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource is not owned by this Partner or does not exist",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "A resource state or idempotency conflict. Branch on error.code: insufficient_balance, out_of_stock, conflict, or idempotency_conflict.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "insufficient_balance": {
                "summary": "Balance cannot cover the charge",
                "value": {
                  "error": {
                    "code": "insufficient_balance",
                    "message": "Insufficient spendable balance."
                  }
                }
              },
              "out_of_stock": {
                "summary": "The vending machine ran out",
                "value": {
                  "error": {
                    "code": "out_of_stock",
                    "message": "Vending machine is out of stock."
                  }
                }
              },
              "conflict": {
                "summary": "A card is not in a state that allows the action",
                "value": {
                  "error": {
                    "code": "conflict",
                    "message": "card_3h7d0 is sealed; reveal it before using it."
                  }
                }
              },
              "idempotency_conflict": {
                "summary": "The key was used for a different request",
                "value": {
                  "error": {
                    "code": "idempotency_conflict",
                    "message": "Idempotency-Key has already been used with different request data."
                  }
                }
              }
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "A required configured dependency is unavailable",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "Account": {
        "type": "object",
        "required": ["id", "name", "createdAt"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^par_"
          },
          "name": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SandboxCredit": {
        "type": "object",
        "required": ["ledgerEntryId", "creditedUsd", "balance"],
        "properties": {
          "ledgerEntryId": { "type": "string" },
          "creditedUsd": { "$ref": "#/components/schemas/Decimal" },
          "balance": { "$ref": "#/components/schemas/Balance" }
        }
      },
      "Balance": {
        "type": "object",
        "required": ["currency", "cash", "bonus", "total"],
        "properties": {
          "currency": {
            "const": "USD"
          },
          "cash": {
            "$ref": "#/components/schemas/Decimal"
          },
          "bonus": {
            "$ref": "#/components/schemas/Decimal"
          },
          "total": {
            "$ref": "#/components/schemas/Decimal"
          }
        }
      },
      "Decimal": {
        "type": "string",
        "pattern": "^-?\\d+\\.\\d{6}$",
        "examples": ["12.500000"]
      },
      "LedgerEntry": {
        "type": "object",
        "required": ["id", "amount", "bucket", "kind", "description", "createdAt"],
        "properties": {
          "id": {
            "type": "string"
          },
          "amount": {
            "$ref": "#/components/schemas/Decimal"
          },
          "bucket": {
            "type": "string",
            "enum": ["CASH", "BONUS"]
          },
          "kind": {
            "type": "string",
            "enum": ["TOPUP", "BONUS", "BUYBACK", "SPEND", "ADJUSTMENT"]
          },
          "description": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ReceivingWallet": {
        "type": "object",
        "required": ["address", "chainId", "asset"],
        "properties": {
          "address": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
          "chainId": { "type": "integer", "enum": [8453, 84532] },
          "asset": { "const": "USDC" }
        }
      },
      "FundingIntentRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["requestedUsd"],
        "properties": {
          "requestedUsd": {
            "type": "string",
            "pattern": "^\\d+(\\.\\d{1,6})?$",
            "description": "Positive USD amount represented with at most six fractional digits."
          }
        }
      },
      "FundingIntent": {
        "type": "object",
        "required": ["id", "requestedUsd", "provider", "status", "chainId", "tokenAddress", "receivingAddress", "createdAt"],
        "properties": {
          "id": { "type": "string", "pattern": "^fint_" },
          "requestedUsd": { "$ref": "#/components/schemas/Decimal" },
          "provider": { "const": "direct_chain" },
          "status": { "const": "created" },
          "chainId": { "type": "integer", "enum": [8453, 84532] },
          "tokenAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
          "receivingAddress": { "type": "string", "pattern": "^0x[0-9a-f]{40}$" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "FundingTransaction": {
        "type": "object",
        "required": ["id", "provider", "status", "chainId", "tokenAddress", "transactionHash", "logIndex", "senderAddress", "receivingAddress", "amountUsd", "confirmedAt", "failureReason", "createdAt"],
        "properties": {
          "id": { "type": "string", "pattern": "^ftx_" },
          "provider": { "const": "direct_chain" },
          "status": { "type": "string", "enum": ["detected", "confirmed", "failed"] },
          "chainId": { "type": "integer", "enum": [8453, 84532] },
          "tokenAddress": { "type": "string", "pattern": "^0x[0-9a-f]{40}$" },
          "transactionHash": { "type": "string", "pattern": "^0x[0-9a-f]{64}$" },
          "logIndex": { "type": "integer", "minimum": 0, "description": "Index of the USDC Transfer log within the transaction. Each log is a separate funding transaction." },
          "senderAddress": { "type": "string", "pattern": "^0x[0-9a-f]{40}$" },
          "receivingAddress": { "type": "string", "pattern": "^0x[0-9a-f]{40}$" },
          "amountUsd": { "$ref": "#/components/schemas/Decimal" },
          "confirmedAt": { "type": ["string", "null"], "format": "date-time" },
          "failureReason": { "type": ["string", "null"] },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "VendingMachine": {
        "type": "object",
        "required": ["id", "name", "description", "image", "imageSrcset", "staticImage", "priceUsd"],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "image": {
            "type": "string",
            "description": "The pack artwork to display, possibly animated. Absolute URL, a WebP rendition 1024px wide where the image service is configured."
          },
          "imageSrcset": {
            "type": ["string", "null"],
            "description": "The same image at 256w, 512w, and 1024w, ready to use as an HTML srcset."
          },
          "staticImage": {
            "type": "string",
            "description": "A still frame for first paint or where animation is unwanted. May be an empty string; fall back to image."
          },
          "priceUsd": {
            "$ref": "#/components/schemas/Decimal"
          }
        }
      },
      "CustomerAttributes": {
        "type": "object",
        "description": "Partner-defined analytics properties, such as plan, country, or acquisition channel. Up to 50 keys of letters, digits, and _ . : - (not starting with $), values nested at most two levels, and 8 KB in total. Do not include contact details or other directly identifying data.",
        "maxProperties": 50,
        "propertyNames": {
          "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]*$",
          "maxLength": 64
        }
      },
      "Customer": {
        "type": "object",
        "required": ["id", "externalUserId", "attributes", "createdAt", "updatedAt"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^cus_"
          },
          "externalUserId": {
            "type": ["string", "null"]
          },
          "attributes": {
            "$ref": "#/components/schemas/CustomerAttributes"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateCustomerRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "externalUserId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "Your own user identifier. Optional: omit it when customers are not tied to a user of yours."
          },
          "attributes": {
            "$ref": "#/components/schemas/CustomerAttributes"
          }
        }
      },
      "UpdateCustomerRequest": {
        "type": "object",
        "additionalProperties": false,
        "minProperties": 1,
        "properties": {
          "externalUserId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "Can be set once on a customer that has none; it cannot be changed afterwards."
          },
          "attributes": {
            "allOf": [{ "$ref": "#/components/schemas/CustomerAttributes" }],
            "description": "Merged into the stored attributes. A key set to null is removed."
          }
        }
      },
      "OffchainOrderRequest": {
        "title": "Order: draw a pack",
        "description": "Charges priceUsd \u00d7 quantity and draws cards. reveal=sealed keeps them hidden and refundable.",
        "type": "object",
        "additionalProperties": false,
        "required": ["kind", "customerId", "vendingMachineId"],
        "properties": {
          "kind": { "const": "order" },
          "mode": { "const": "offchain", "default": "offchain", "description": "Optional: draws are always off-chain. Mint the cards afterwards with kind=mint." },
          "customerId": { "type": "string", "pattern": "^cus_" },
          "vendingMachineId": { "type": "integer", "minimum": 1 },
          "quantity": { "type": "integer", "minimum": 1, "maximum": 10, "default": 1 },
          "reveal": {
            "type": "string",
            "enum": ["on_create", "sealed"],
            "default": "on_create",
            "description": "on_create reveals the cards at once and they cannot be refunded. sealed hides them; a sealed card can be refunded until it is revealed or its refund window closes."
          },
          "metadata": {}
        }
      },
      "MintRequest": {
        "title": "Mint: withdraw held cards as NFTs",
        "description": "Returns awaiting_chain with calldata for the customer's wallet to submit.",
        "type": "object",
        "additionalProperties": false,
        "required": ["kind", "customerId", "cardIds", "walletAddress"],
        "description": "Withdraws held, revealed cards as NFTs. Nothing is charged. If a signature expires, re-sign (same token) or cancel (the card stays held).",
        "properties": {
          "kind": { "const": "mint" },
          "mode": { "const": "onchain" },
          "customerId": { "type": "string", "pattern": "^cus_" },
          "cardIds": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "uniqueItems": true,
            "items": { "type": "string", "pattern": "^card_" }
          },
          "walletAddress": {
            "type": "string",
            "pattern": "^0x[0-9a-fA-F]{40}$",
            "description": "The wallet that receives the NFTs, on the environment's chain. The transaction must be sent from this address."
          },
          "metadata": {}
        }
      },
      "RefundRequest": {
        "title": "Refund: return sealed cards",
        "description": "Only sealed cards before refundableUntil. Credits each card's share of the price back.",
        "type": "object",
        "additionalProperties": false,
        "required": ["kind", "customerId", "cardIds"],
        "description": "Refunds sealed cards within their refund window and returns them to inventory.",
        "properties": {
          "kind": { "const": "refund" },
          "mode": { "const": "offchain" },
          "customerId": { "type": "string", "pattern": "^cus_" },
          "cardIds": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "uniqueItems": true,
            "items": { "type": "string", "pattern": "^card_" }
          },
          "metadata": {}
        }
      },
      "OnchainTransactionSubmissionRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["transactionHash"],
        "properties": {
          "transactionHash": {
            "type": "string",
            "pattern": "^0x[0-9a-fA-F]{64}$"
          }
        }
      },
      "OffchainBuybackRequest": {
        "title": "Off-chain buyback: sell held cards",
        "description": "Cards must be held: status active, sealed false, onchain null. Credits the current buybackUsd total.",
        "type": "object",
        "additionalProperties": false,
        "required": ["kind", "mode", "customerId", "cardIds"],
        "properties": {
          "kind": {
            "const": "buyback"
          },
          "mode": {
            "const": "offchain"
          },
          "customerId": {
            "type": "string",
            "pattern": "^cus_"
          },
          "cardIds": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "pattern": "^card_"
            }
          },
          "metadata": {}
        }
      },
      "OnchainBuybackRequest": {
        "title": "On-chain buyback: sell minted cards",
        "description": "Cards must be minted (onchain.mintedAt set). Completes when the burn confirms.",
        "type": "object",
        "additionalProperties": false,
        "required": ["kind", "mode", "customerId", "cardIds"],
        "description": "Buys back minted cards. Each card gets a handler burn authorization in onchainBurns; the credit is posted when the burn confirms. Held cards use mode offchain.",
        "properties": {
          "kind": { "const": "buyback" },
          "mode": { "const": "onchain" },
          "customerId": { "type": "string", "pattern": "^cus_" },
          "cardIds": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "uniqueItems": true,
            "items": { "type": "string", "pattern": "^card_" }
          },
          "metadata": {}
        }
      },
      "OffchainRedemptionRequest": {
        "title": "Redemption: ship held cards",
        "description": "Cards must be held: status active, sealed false, onchain null. Charges the shipping fee for shipmentInfo.country.",
        "type": "object",
        "additionalProperties": false,
        "required": ["kind", "mode", "customerId", "cardIds", "shipmentInfo"],
        "properties": {
          "kind": {
            "const": "redemption"
          },
          "mode": {
            "const": "offchain"
          },
          "customerId": {
            "type": "string",
            "pattern": "^cus_"
          },
          "cardIds": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "pattern": "^card_"
            }
          },
          "shipmentInfo": {
            "type": "object",
            "required": ["country"],
            "properties": {
              "country": { "type": "string", "minLength": 1, "maxLength": 100, "description": "English country name or ISO 3166-1 alpha-2 code, for example \"Japan\" or \"JP\". Unrecognized values return 400; see GET /api/v2/shipping-countries." },
              "name": { "type": "string", "description": "Recipient's full name. Needed to ship." },
              "phone": { "type": "string", "description": "Phone number with country code. Needed to ship." },
              "address1": { "type": "string", "description": "Street address. Needed to ship." },
              "address2": { "type": "string", "description": "Building, apartment, or unit." },
              "city": { "type": "string", "description": "City or locality. Needed to ship." },
              "state": { "type": "string", "description": "State, province, or prefecture, where the country uses one." },
              "postalCode": { "type": "string", "description": "Postal code, where the country uses one." }
            },
            "description": "Stored with the shipment, up to 20 KB. Only country is validated; Packflip's fulfilment team reads the other fields, so send all that apply. Undeliverable addresses move the shipment to exception.",
            "additionalProperties": true
          },
          "notifications": {
            "$ref": "#/components/schemas/RedemptionNotifications"
          },
          "metadata": {}
        }
      },
      "ShipmentUpdateRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["status"],
        "properties": {
          "status": {
            "type": "string",
            "enum": ["created", "shipped", "in_transit", "delivered", "exception"]
          },
          "carrier": {
            "type": ["string", "null"],
            "minLength": 1,
            "maxLength": 160
          },
          "trackingNumber": {
            "type": ["string", "null"],
            "minLength": 1,
            "maxLength": 255
          },
          "trackingUrl": {
            "type": ["string", "null"],
            "format": "uri",
            "pattern": "^https://",
            "maxLength": 2048
          },
          "statusReason": {
            "type": ["string", "null"],
            "minLength": 1,
            "maxLength": 1000
          },
          "shipmentInfo": {
            "type": "object"
          }
        }
      },
      "RedemptionNotifications": {
        "type": "object",
        "additionalProperties": false,
        "required": ["channels"],
        "description": "Optional shipment emails. Webhooks are always sent. Emails go out when the shipment is created, shipped, delivered, or hits an exception. The sandbox sends partner_email only.",
        "properties": {
          "channels": {
            "type": "array",
            "uniqueItems": true,
            "items": {
              "type": "string",
              "enum": ["customer_email", "partner_email"]
            },
            "description": "customer_email sends to customerEmail. partner_email sends to the notification email set in the Partner Console; the request fails with 400 if none is set."
          },
          "customerEmail": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "Required with customer_email, and only then."
          }
        }
      },
      "Shipment": {
        "type": "object",
        "required": ["id", "status", "carrier", "trackingNumber", "trackingUrl", "statusReason", "shipmentInfo", "addressUpdatedAt", "notifications", "createdAt", "updatedAt"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^shp_"
          },
          "status": {
            "type": "string",
            "enum": ["created", "shipped", "in_transit", "delivered", "exception"]
          },
          "carrier": {
            "type": ["string", "null"]
          },
          "trackingNumber": {
            "type": ["string", "null"]
          },
          "trackingUrl": {
            "type": ["string", "null"],
            "format": "uri"
          },
          "statusReason": {
            "type": ["string", "null"]
          },
          "shipmentInfo": {
            "type": "object"
          },
          "addressUpdatedAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When the partner last corrected the address."
          },
          "notifications": {
            "type": "object",
            "required": ["channels", "customerEmail"],
            "properties": {
              "channels": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": ["customer_email", "partner_email"]
                }
              },
              "customerEmail": {
                "type": ["string", "null"],
                "format": "email"
              },
              "emails": {
                "type": "array",
                "description": "Emails queued so far, one per channel and shipment status.",
                "items": {
                  "type": "object",
                  "required": ["channel", "shipmentStatus", "status", "sentAt", "createdAt"],
                  "properties": {
                    "channel": {
                      "type": "string",
                      "enum": ["customer_email", "partner_email"]
                    },
                    "shipmentStatus": {
                      "type": "string",
                      "enum": ["created", "shipped", "delivered", "exception"]
                    },
                    "status": {
                      "type": "string",
                      "enum": ["pending", "sent", "failed", "skipped"]
                    },
                    "sentAt": {
                      "type": ["string", "null"],
                      "format": "date-time"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateWebhookRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["url", "subscribedEvents"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://",
            "maxLength": 2048
          },
          "subscribedEvents": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "type": "string",
              "pattern": "^(\\*|[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+)$",
              "maxLength": 120
            }
          }
        }
      },
      "UpdateWebhookRequest": {
        "type": "object",
        "additionalProperties": false,
        "minProperties": 1,
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://",
            "maxLength": 2048
          },
          "subscribedEvents": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "type": "string",
              "pattern": "^(\\*|[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+)$",
              "maxLength": 120
            }
          },
          "active": { "type": "boolean" },
          "rotateSigningSecret": { "type": "boolean" }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "required": ["id", "url", "subscribedEvents", "active", "lastValidatedAt", "createdAt", "updatedAt"],
        "properties": {
          "id": { "type": "string", "pattern": "^wh_" },
          "url": { "type": "string", "format": "uri" },
          "subscribedEvents": {
            "type": "array",
            "items": { "type": "string" }
          },
          "active": { "type": "boolean" },
          "lastValidatedAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "WebhookEndpointWithSecret": {
        "allOf": [
          { "$ref": "#/components/schemas/WebhookEndpoint" },
          {
            "type": "object",
            "properties": {
              "signingSecret": {
                "type": "string",
                "description": "Returned only after creation or explicit rotation. Store it before discarding the response."
              }
            }
          }
        ]
      },
      "WebhookDelivery": {
        "type": "object",
        "required": ["id", "eventId", "eventType", "status", "attempts", "responseCode", "lastError", "nextAttemptAt", "deliveredAt", "createdAt", "updatedAt"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "eventId": { "type": "string", "pattern": "^evt_" },
          "eventType": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "processing", "delivered", "failed"] },
          "attempts": { "type": "integer", "minimum": 0 },
          "responseCode": { "type": ["integer", "null"] },
          "lastError": { "type": ["string", "null"] },
          "nextAttemptAt": { "type": ["string", "null"], "format": "date-time" },
          "deliveredAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "ReplayWebhookRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["eventId"],
        "properties": {
          "eventId": { "type": "string", "pattern": "^evt_" }
        }
      },
      "Card": {
        "description": "Every action needs status=active. Sealed (sealed=true): reveal, or refund before refundableUntil. Held (sealed=false, onchain=null): off-chain buyback, redemption, or mint. Mint pending (onchain set, onchain.mintedAt null): submit or cancel the mint. Minted (onchain.mintedAt set): on-chain buyback.",
        "type": "object",
        "required": [
          "id",
          "status",
          "sealed",
          "refundableUntil",
          "name",
          "image",
          "imageSrcset",
          "buybackUsd",
          "onchain",
          "metadata",
          "operationId",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": { "type": "string", "pattern": "^card_" },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "reserved",
              "redeemed",
              "bought_back",
              "refunded",
              "buyback_pending",
              "cancelled"
            ]
          },
          "sealed": {
            "type": "boolean",
            "description": "True while the card is hidden and refundable. Name, image, buybackUsd, and metadata are null while sealed."
          },
          "refundableUntil": { "type": ["string", "null"], "format": "date-time" },
          "name": { "type": ["string", "null"] },
          "image": {
            "type": ["string", "null"],
            "description": "Absolute URL of the card art, a WebP rendition 1024px wide where the image service is configured. Null while the card is sealed."
          },
          "imageSrcset": {
            "type": ["string", "null"],
            "description": "The same art at 256w, 512w, and 1024w, ready to use as an HTML srcset. Null while the card is sealed, and for art the image service does not render."
          },
          "buybackUsd": { "oneOf": [{ "$ref": "#/components/schemas/Decimal" }, { "type": "null" }] },
          "onchain": { "oneOf": [{ "$ref": "#/components/schemas/CardOnchain" }, { "type": "null" }] },
          "metadata": {},
          "operationId": { "type": "string", "pattern": "^op_" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "CardOnchain": {
        "type": "object",
        "required": ["chainId", "collectionAddress", "tokenId", "mintedAt"],
        "description": "Present once a mint is authorized. mintedAt is null until the NFT is minted.",
        "properties": {
          "chainId": { "type": "integer" },
          "collectionAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
          "tokenId": { "type": "string", "pattern": "^[0-9]+$" },
          "mintedAt": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "Operation": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "mode",
          "status",
          "customerId",
          "vendingMachineId",
          "quantity",
          "chargedUsd",
          "creditedUsd",
          "metadata",
          "completedAt",
          "createdAt",
          "updatedAt",
          "shipment",
          "cards",
          "affectedCards",
          "onchainOrders",
          "onchainBurns"
        ],
        "properties": {
          "id": { "type": "string", "pattern": "^op_" },
          "kind": { "type": "string", "enum": ["order", "buyback", "redemption", "mint", "refund"] },
          "mode": { "type": "string", "enum": ["offchain", "onchain"] },
          "status": {
            "type": "string",
            "enum": ["pending", "awaiting_chain", "completed", "failed", "cancelled"]
          },
          "customerId": { "type": "string", "pattern": "^cus_" },
          "vendingMachineId": { "type": ["integer", "null"] },
          "quantity": { "type": "integer" },
          "chargedUsd": { "$ref": "#/components/schemas/Decimal" },
          "creditedUsd": { "$ref": "#/components/schemas/Decimal" },
          "metadata": {},
          "completedAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" },
          "shipment": { "oneOf": [{ "$ref": "#/components/schemas/Shipment" }, { "type": "null" }] },
          "cards": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/OperationCard" }
          },
          "affectedCards": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/OperationCard" }
          },
          "onchainOrders": {
            "type": "array",
            "description": "Mint authorizations, one per card.",
            "items": { "$ref": "#/components/schemas/OnchainOrder" }
          },
          "onchainBurns": { "type": "array", "items": { "$ref": "#/components/schemas/OnchainBurn" } }
        }
      },
      "OnchainOrder": {
        "type": "object",
        "required": [
          "id",
          "cardId",
          "chainId",
          "vendingMachineAddress",
          "collectionAddress",
          "tokenId",
          "buyerAddress",
          "paymentTokenAddress",
          "priceBaseUnits",
          "deadline",
          "signature",
          "calldata",
          "transactionHash",
          "submittedAt",
          "confirmedAt",
          "cancelledAt",
          "failureReason"
        ],
        "properties": {
          "id": { "type": "string", "pattern": "^oco_" },
          "cardId": { "type": ["string", "null"], "pattern": "^card_" },
          "chainId": { "type": "integer" },
          "vendingMachineAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
          "collectionAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
          "tokenId": { "type": "string", "pattern": "^[0-9]+$" },
          "buyerAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
          "paymentTokenAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
          "priceBaseUnits": { "type": "string", "pattern": "^[0-9]+$" },
          "deadline": { "type": "string", "format": "date-time" },
          "signature": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$" },
          "calldata": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$" },
          "transactionHash": { "type": ["string", "null"], "pattern": "^0x[0-9a-fA-F]{64}$" },
          "submittedAt": { "type": ["string", "null"], "format": "date-time" },
          "confirmedAt": { "type": ["string", "null"], "format": "date-time" },
          "cancelledAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Set when the mint was cancelled after its signature expired; the card stays held."
          },
          "failureReason": { "type": ["string", "null"] }
        }
      },
      "OnchainBurn": {
        "type": "object",
        "required": [
          "id",
          "cardId",
          "chainId",
          "handlerAddress",
          "collectionAddress",
          "tokenId",
          "requestId",
          "deadline",
          "signature",
          "calldata",
          "transactionHash",
          "submittedAt",
          "confirmedAt",
          "cancelledAt",
          "failureReason"
        ],
        "description": "A handler burn authorization for an on-chain buyback. Before submitting calldata, the holder must approve handlerAddress on the collection (setApprovalForAll).",
        "properties": {
          "id": { "type": "string", "pattern": "^ocb_" },
          "cardId": { "type": "string", "pattern": "^card_" },
          "chainId": { "type": "integer" },
          "handlerAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
          "collectionAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
          "tokenId": { "type": "string", "pattern": "^[0-9]+$" },
          "requestId": { "type": "string", "pattern": "^0x[0-9a-fA-F]{64}$" },
          "deadline": { "type": "string", "format": "date-time" },
          "signature": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$" },
          "calldata": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$" },
          "transactionHash": { "type": ["string", "null"], "pattern": "^0x[0-9a-fA-F]{64}$" },
          "submittedAt": { "type": ["string", "null"], "format": "date-time" },
          "confirmedAt": { "type": ["string", "null"], "format": "date-time" },
          "cancelledAt": { "type": ["string", "null"], "format": "date-time" },
          "failureReason": { "type": ["string", "null"] }
        }
      },
      "OperationCard": {
        "type": "object",
        "required": ["id", "status", "sealed", "refundableUntil", "name", "image", "imageSrcset", "buybackUsd", "onchain"],
        "properties": {
          "id": { "type": "string", "pattern": "^card_" },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "reserved",
              "redeemed",
              "bought_back",
              "refunded",
              "buyback_pending",
              "cancelled"
            ]
          },
          "sealed": {
            "type": "boolean",
            "description": "True while the card is hidden and refundable. Name, image, buybackUsd, and metadata are null while sealed."
          },
          "refundableUntil": { "type": ["string", "null"], "format": "date-time" },
          "name": { "type": ["string", "null"] },
          "image": {
            "type": ["string", "null"],
            "description": "Absolute URL of the card art, a WebP rendition 1024px wide where the image service is configured. Null while the card is sealed."
          },
          "imageSrcset": {
            "type": ["string", "null"],
            "description": "The same art at 256w, 512w, and 1024w, ready to use as an HTML srcset. Null while the card is sealed, and for art the image service does not render."
          },
          "buybackUsd": { "oneOf": [{ "$ref": "#/components/schemas/Decimal" }, { "type": "null" }] },
          "onchain": { "oneOf": [{ "$ref": "#/components/schemas/CardOnchain" }, { "type": "null" }] }
        }
      },
      "OperationResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Operation"
          },
          {
            "type": "object",
            "required": ["replayed"],
            "properties": {
              "replayed": {
                "type": "boolean"
              }
            }
          }
        ]
      }
    }
  }
}
