From 9b7a79d8a7c9e07363852e71b1bd0f23e11090ee Mon Sep 17 00:00:00 2001 From: Alex Pate Date: Thu, 8 Oct 2026 15:53:45 +0200 Subject: [PATCH 1/8] Generate types from the OpenAPI spec --- .github/workflows/ci.yml | 1 + biome.json | 10 +- openapi.json | 7885 ++++++++++++++++++++++++++++++++++ package.json | 6 +- pnpm-lock.yaml | 212 +- scripts/gen-event-types.mjs | 22 + scripts/sync-spec.mjs | 22 + src/generated/event-types.ts | 15 + src/generated/openapi.ts | 3950 +++++++++++++++++ test/generated.test.ts | 11 + 10 files changed, 12127 insertions(+), 7 deletions(-) create mode 100644 openapi.json create mode 100644 scripts/gen-event-types.mjs create mode 100644 scripts/sync-spec.mjs create mode 100644 src/generated/event-types.ts create mode 100644 src/generated/openapi.ts create mode 100644 test/generated.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1854a4d..e01cba5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: node-version: '22' cache: pnpm - run: pnpm install --frozen-lockfile + - run: pnpm generate:check - run: pnpm lint - run: pnpm type-check - run: pnpm build diff --git a/biome.json b/biome.json index 9a2a11c..1dc8500 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,15 @@ "useIgnoreFile": true }, "files": { - "ignore": ["dist", "node_modules", ".turbo", "coverage", "ecosystem-tests"] + "ignore": [ + "dist", + "node_modules", + ".turbo", + "coverage", + "ecosystem-tests", + "src/generated", + "openapi.json" + ] }, "linter": { "enabled": true, diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..2b4e447 --- /dev/null +++ b/openapi.json @@ -0,0 +1,7885 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Passmint API", + "version": "1.0.0", + "description": "Issue and manage Apple Wallet and Google Wallet passes. All endpoints are authenticated with a secret API key." + }, + "servers": [ + { + "url": "https://api.passmint.com" + } + ], + "components": { + "securitySchemes": { + "scannerToken": { + "type": "http", + "scheme": "bearer", + "description": "A scanner device token, obtained from POST /v1/scanners/pair." + }, + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "description": "Your Passmint secret API key, e.g. `pmk_live_…` or `pmk_test_…`. Test keys never touch live data." + } + }, + "schemas": { + "CreateTemplateBody": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "event", + "membership", + "coupon", + "loyalty", + "generic" + ] + }, + "apple_style": { + "type": "string", + "enum": [ + "generic", + "eventTicket", + "boardingPass", + "coupon", + "storeCard" + ] + }, + "design": { + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "logoText": { + "type": [ + "string", + "null" + ] + }, + "foregroundColor": { + "type": "string", + "pattern": "^(rgb\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*\\)|#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)$" + }, + "backgroundColor": { + "type": "string", + "pattern": "^(rgb\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*\\)|#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)$" + }, + "labelColor": { + "type": "string", + "pattern": "^(rgb\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*\\)|#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)$" + }, + "logoImageKey": { + "type": [ + "string", + "null" + ], + "description": "Storage key from an image upload. Leave null to keep none." + }, + "iconImageKey": { + "type": [ + "string", + "null" + ], + "description": "Storage key from an image upload. Leave null to keep none." + }, + "stripImageKey": { + "type": [ + "string", + "null" + ], + "description": "Storage key from an image upload. Leave null to keep none." + }, + "thumbnailImageKey": { + "type": [ + "string", + "null" + ], + "description": "Storage key from an image upload. Leave null to keep none." + }, + "headerFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design." + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "textAlignment": { + "type": [ + "string", + "null" + ], + "enum": [ + "left", + "center", + "right", + "natural", + null + ] + }, + "required": {}, + "changeMessage": { + "type": [ + "string", + "null" + ], + "description": "Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder." + } + }, + "required": [ + "key" + ] + } + }, + "primaryFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design." + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "textAlignment": { + "type": [ + "string", + "null" + ], + "enum": [ + "left", + "center", + "right", + "natural", + null + ] + }, + "required": {}, + "changeMessage": { + "type": [ + "string", + "null" + ], + "description": "Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder." + } + }, + "required": [ + "key" + ] + } + }, + "secondaryFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design." + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "textAlignment": { + "type": [ + "string", + "null" + ], + "enum": [ + "left", + "center", + "right", + "natural", + null + ] + }, + "required": {}, + "changeMessage": { + "type": [ + "string", + "null" + ], + "description": "Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder." + } + }, + "required": [ + "key" + ] + } + }, + "auxiliaryFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design." + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "textAlignment": { + "type": [ + "string", + "null" + ], + "enum": [ + "left", + "center", + "right", + "natural", + null + ] + }, + "required": {}, + "changeMessage": { + "type": [ + "string", + "null" + ], + "description": "Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder." + } + }, + "required": [ + "key" + ] + } + }, + "backFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design." + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "textAlignment": { + "type": [ + "string", + "null" + ], + "enum": [ + "left", + "center", + "right", + "natural", + null + ] + }, + "required": {}, + "changeMessage": { + "type": [ + "string", + "null" + ], + "description": "Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder." + } + }, + "required": [ + "key" + ] + } + }, + "barcodeFormat": { + "type": [ + "string", + "null" + ], + "enum": [ + "PKBarcodeFormatQR", + "PKBarcodeFormatPDF417", + "PKBarcodeFormatAztec", + "PKBarcodeFormatCode128", + null + ] + }, + "barcodeMessageTemplate": { + "type": [ + "string", + "null" + ], + "description": "What the barcode encodes. Use '{{serialNumber}}' — scanning looks passes up by serial number, and it is unique per pass. '{{fieldValues.}}' inserts a field value. No other placeholders exist." + }, + "issuerName": { + "type": [ + "string", + "null" + ], + "maxLength": 64, + "description": "The issuer shown on the pass and in its notifications (Apple organizationName; Google issuer/program name unless logoText is set). Defaults to your organization's name." + }, + "locations": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "type": "number" + }, + "relevantText": { + "type": "string", + "maxLength": 256, + "description": "Lock-screen text Apple Wallet shows near this location." + } + }, + "required": [ + "latitude", + "longitude" + ] + }, + "maxItems": 10, + "description": "Up to 10 places the pass is relevant near. Apple Wallet suggests the pass on the lock screen there; Google Wallet stores them on the pass." + }, + "relevantDate": { + "type": [ + "string", + "null" + ], + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?(Z|[+-]\\d{2}:\\d{2})$", + "description": "ISO 8601 with offset. Apple Wallet surfaces the pass around this time. Apple only." + } + }, + "required": [ + "foregroundColor", + "backgroundColor", + "labelColor" + ] + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "apple", + "google" + ] + } + }, + "starter_template_id": { + "type": "string" + }, + "require_download_link": { + "type": "boolean" + }, + "redemption_policy": { + "$ref": "#/components/schemas/RedemptionPolicyInput" + } + }, + "required": [ + "name", + "type", + "apple_style", + "design" + ] + }, + "RedemptionPolicyInput": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "single_use", + "reusable", + "count" + ] + }, + "max_uses": { + "type": "integer", + "minimum": 1, + "description": "Required when mode is count." + } + }, + "required": [ + "mode" + ] + }, + "UpdateTemplateBody": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "design": { + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "logoText": { + "type": [ + "string", + "null" + ] + }, + "foregroundColor": { + "type": "string", + "pattern": "^(rgb\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*\\)|#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)$" + }, + "backgroundColor": { + "type": "string", + "pattern": "^(rgb\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*\\)|#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)$" + }, + "labelColor": { + "type": "string", + "pattern": "^(rgb\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*\\)|#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)$" + }, + "logoImageKey": { + "type": [ + "string", + "null" + ], + "description": "Storage key from an image upload. Leave null to keep none." + }, + "iconImageKey": { + "type": [ + "string", + "null" + ], + "description": "Storage key from an image upload. Leave null to keep none." + }, + "stripImageKey": { + "type": [ + "string", + "null" + ], + "description": "Storage key from an image upload. Leave null to keep none." + }, + "thumbnailImageKey": { + "type": [ + "string", + "null" + ], + "description": "Storage key from an image upload. Leave null to keep none." + }, + "headerFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design." + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "textAlignment": { + "type": [ + "string", + "null" + ], + "enum": [ + "left", + "center", + "right", + "natural", + null + ] + }, + "required": {}, + "changeMessage": { + "type": [ + "string", + "null" + ], + "description": "Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder." + } + }, + "required": [ + "key" + ] + } + }, + "primaryFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design." + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "textAlignment": { + "type": [ + "string", + "null" + ], + "enum": [ + "left", + "center", + "right", + "natural", + null + ] + }, + "required": {}, + "changeMessage": { + "type": [ + "string", + "null" + ], + "description": "Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder." + } + }, + "required": [ + "key" + ] + } + }, + "secondaryFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design." + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "textAlignment": { + "type": [ + "string", + "null" + ], + "enum": [ + "left", + "center", + "right", + "natural", + null + ] + }, + "required": {}, + "changeMessage": { + "type": [ + "string", + "null" + ], + "description": "Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder." + } + }, + "required": [ + "key" + ] + } + }, + "auxiliaryFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design." + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "textAlignment": { + "type": [ + "string", + "null" + ], + "enum": [ + "left", + "center", + "right", + "natural", + null + ] + }, + "required": {}, + "changeMessage": { + "type": [ + "string", + "null" + ], + "description": "Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder." + } + }, + "required": [ + "key" + ] + } + }, + "backFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design." + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "textAlignment": { + "type": [ + "string", + "null" + ], + "enum": [ + "left", + "center", + "right", + "natural", + null + ] + }, + "required": {}, + "changeMessage": { + "type": [ + "string", + "null" + ], + "description": "Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder." + } + }, + "required": [ + "key" + ] + } + }, + "barcodeFormat": { + "type": [ + "string", + "null" + ], + "enum": [ + "PKBarcodeFormatQR", + "PKBarcodeFormatPDF417", + "PKBarcodeFormatAztec", + "PKBarcodeFormatCode128", + null + ] + }, + "barcodeMessageTemplate": { + "type": [ + "string", + "null" + ], + "description": "What the barcode encodes. Use '{{serialNumber}}' — scanning looks passes up by serial number, and it is unique per pass. '{{fieldValues.}}' inserts a field value. No other placeholders exist." + }, + "issuerName": { + "type": [ + "string", + "null" + ], + "maxLength": 64, + "description": "The issuer shown on the pass and in its notifications (Apple organizationName; Google issuer/program name unless logoText is set). Defaults to your organization's name." + }, + "locations": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "type": "number" + }, + "relevantText": { + "type": "string", + "maxLength": 256, + "description": "Lock-screen text Apple Wallet shows near this location." + } + }, + "required": [ + "latitude", + "longitude" + ] + }, + "maxItems": 10, + "description": "Up to 10 places the pass is relevant near. Apple Wallet suggests the pass on the lock screen there; Google Wallet stores them on the pass." + }, + "relevantDate": { + "type": [ + "string", + "null" + ], + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?(Z|[+-]\\d{2}:\\d{2})$", + "description": "ISO 8601 with offset. Apple Wallet surfaces the pass around this time. Apple only." + } + }, + "required": [ + "foregroundColor", + "backgroundColor", + "labelColor" + ] + }, + "archived": { + "type": "boolean" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "apple", + "google" + ] + } + }, + "certificate_set_id": { + "type": [ + "string", + "null" + ] + }, + "google_issuer_id": { + "type": [ + "string", + "null" + ] + }, + "require_download_link": { + "type": "boolean" + }, + "redemption_policy": { + "$ref": "#/components/schemas/RedemptionPolicyInput" + } + } + }, + "CreateWebhookBody": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The HTTPS URL events are POSTed to.", + "example": "https://example.com/hooks/passmint" + }, + "events": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required, non-empty. Event types to deliver, or `*` for all.", + "example": [ + "pass.redeemed" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "enabled": { + "type": "boolean", + "description": "Defaults to true." + } + }, + "required": [ + "url", + "events" + ] + }, + "UpdateWebhookBody": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Event types to deliver (for example `pass.redeemed`), or `*` for all of them.", + "example": [ + "pass.redeemed" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "enabled": { + "type": "boolean" + } + } + }, + "BackfillBody": { + "type": "object", + "properties": { + "since": { + "type": "string", + "description": "ISO 8601 start of the window. Required.", + "example": "2026-10-01T00:00:00Z" + }, + "until": { + "type": "string", + "description": "ISO 8601 end of the window. Defaults to now. The window is capped in length." + } + }, + "required": [ + "since" + ] + }, + "PassIssuedEvent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "type": { + "type": "string", + "enum": [ + "pass.issued" + ] + }, + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "idempotency_key": { + "type": "string" + }, + "livemode": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "object": { + "type": "object", + "properties": { + "pass": { + "$ref": "#/components/schemas/EventPassSnapshot" + } + }, + "required": [ + "pass" + ] + } + }, + "required": [ + "object" + ] + }, + "source": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "unit": { + "type": [ + "string", + "null" + ], + "enum": [ + "device", + "object", + null + ] + }, + "confidence": { + "type": "string", + "enum": [ + "exact", + "best_effort", + "unconfirmed" + ] + } + }, + "required": [ + "platform", + "unit", + "confidence" + ] + }, + "previous_attributes": { + "type": "null" + } + }, + "required": [ + "id", + "object", + "type", + "api_version", + "created_at", + "idempotency_key", + "livemode", + "data", + "source", + "previous_attributes" + ] + }, + "EventPassSnapshot": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "short_id": { + "type": "string" + }, + "template_id": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "serial_number": { + "type": "string" + }, + "holder": { + "type": "object", + "properties": { + "email_hash": { + "type": [ + "string", + "null" + ] + }, + "name_present": { + "type": "boolean" + } + }, + "required": [ + "email_hash", + "name_present" + ] + }, + "voided": { + "type": "boolean" + }, + "image_variant": { + "type": [ + "string", + "null" + ], + "description": "The template image variant the pass renders with, or null for the template's own images." + }, + "url": { + "type": "string" + }, + "download_url": { + "type": [ + "string", + "null" + ] + }, + "google_wallet_url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "short_id", + "template_id", + "organization_id", + "serial_number", + "holder", + "voided", + "image_variant", + "url", + "download_url", + "google_wallet_url" + ] + }, + "PassAddIntentEvent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "type": { + "type": "string", + "enum": [ + "pass.add_intent" + ] + }, + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "idempotency_key": { + "type": "string" + }, + "livemode": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "object": { + "type": "object", + "properties": { + "pass": { + "$ref": "#/components/schemas/EventPassSnapshot" + } + }, + "required": [ + "pass" + ] + } + }, + "required": [ + "object" + ] + }, + "source": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "unit": { + "type": [ + "string", + "null" + ], + "enum": [ + "device", + "object", + null + ] + }, + "confidence": { + "type": "string", + "enum": [ + "exact", + "best_effort", + "unconfirmed" + ] + } + }, + "required": [ + "platform", + "unit", + "confidence" + ] + }, + "previous_attributes": { + "type": "null" + } + }, + "required": [ + "id", + "object", + "type", + "api_version", + "created_at", + "idempotency_key", + "livemode", + "data", + "source", + "previous_attributes" + ] + }, + "PassAddedToWalletEvent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "type": { + "type": "string", + "enum": [ + "pass.added_to_wallet" + ] + }, + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "idempotency_key": { + "type": "string" + }, + "livemode": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "object": { + "type": "object", + "properties": { + "pass": { + "$ref": "#/components/schemas/EventPassSnapshot" + } + }, + "required": [ + "pass" + ] + } + }, + "required": [ + "object" + ] + }, + "source": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "unit": { + "type": [ + "string", + "null" + ], + "enum": [ + "device", + "object", + null + ] + }, + "confidence": { + "type": "string", + "enum": [ + "exact", + "best_effort", + "unconfirmed" + ] + } + }, + "required": [ + "platform", + "unit", + "confidence" + ] + }, + "previous_attributes": { + "type": "null" + } + }, + "required": [ + "id", + "object", + "type", + "api_version", + "created_at", + "idempotency_key", + "livemode", + "data", + "source", + "previous_attributes" + ] + }, + "PassUpdatePushedEvent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "type": { + "type": "string", + "enum": [ + "pass.update_pushed" + ] + }, + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "idempotency_key": { + "type": "string" + }, + "livemode": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "object": { + "type": "object", + "properties": { + "pass": { + "$ref": "#/components/schemas/EventPassSnapshot" + } + }, + "required": [ + "pass" + ] + } + }, + "required": [ + "object" + ] + }, + "source": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "unit": { + "type": [ + "string", + "null" + ], + "enum": [ + "device", + "object", + null + ] + }, + "confidence": { + "type": "string", + "enum": [ + "exact", + "best_effort", + "unconfirmed" + ] + } + }, + "required": [ + "platform", + "unit", + "confidence" + ] + }, + "previous_attributes": { + "type": "null" + } + }, + "required": [ + "id", + "object", + "type", + "api_version", + "created_at", + "idempotency_key", + "livemode", + "data", + "source", + "previous_attributes" + ] + }, + "PassUpdateDeliveredEvent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "type": { + "type": "string", + "enum": [ + "pass.update_delivered" + ] + }, + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "idempotency_key": { + "type": "string" + }, + "livemode": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "object": { + "type": "object", + "properties": { + "pass": { + "$ref": "#/components/schemas/EventPassSnapshot" + } + }, + "required": [ + "pass" + ] + } + }, + "required": [ + "object" + ] + }, + "source": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "unit": { + "type": [ + "string", + "null" + ], + "enum": [ + "device", + "object", + null + ] + }, + "confidence": { + "type": "string", + "enum": [ + "exact", + "best_effort", + "unconfirmed" + ] + } + }, + "required": [ + "platform", + "unit", + "confidence" + ] + }, + "previous_attributes": { + "type": "null" + } + }, + "required": [ + "id", + "object", + "type", + "api_version", + "created_at", + "idempotency_key", + "livemode", + "data", + "source", + "previous_attributes" + ] + }, + "PassUpdateFailedEvent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "type": { + "type": "string", + "enum": [ + "pass.update_failed" + ] + }, + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "idempotency_key": { + "type": "string" + }, + "livemode": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "object": { + "type": "object", + "properties": { + "pass": { + "$ref": "#/components/schemas/EventPassSnapshot" + }, + "delivery_failure": { + "$ref": "#/components/schemas/EventDeliveryFailure" + } + }, + "required": [ + "pass", + "delivery_failure" + ] + } + }, + "required": [ + "object" + ] + }, + "source": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "unit": { + "type": [ + "string", + "null" + ], + "enum": [ + "device", + "object", + null + ] + }, + "confidence": { + "type": "string", + "enum": [ + "exact", + "best_effort", + "unconfirmed" + ] + } + }, + "required": [ + "platform", + "unit", + "confidence" + ] + }, + "previous_attributes": { + "type": "null" + } + }, + "required": [ + "id", + "object", + "type", + "api_version", + "created_at", + "idempotency_key", + "livemode", + "data", + "source", + "previous_attributes" + ] + }, + "EventDeliveryFailure": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "failed": { + "type": [ + "number", + "null" + ] + }, + "devices": { + "type": [ + "number", + "null" + ] + }, + "context": { + "type": [ + "string", + "null" + ], + "enum": [ + "update", + "void", + null + ] + } + }, + "required": [ + "platform", + "reason", + "failed", + "devices", + "context" + ], + "description": "`data.object.delivery_failure`, present on `pass.update_failed`." + }, + "PassRemovedEvent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "type": { + "type": "string", + "enum": [ + "pass.removed" + ] + }, + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "idempotency_key": { + "type": "string" + }, + "livemode": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "object": { + "type": "object", + "properties": { + "pass": { + "$ref": "#/components/schemas/EventPassSnapshot" + } + }, + "required": [ + "pass" + ] + } + }, + "required": [ + "object" + ] + }, + "source": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "unit": { + "type": [ + "string", + "null" + ], + "enum": [ + "device", + "object", + null + ] + }, + "confidence": { + "type": "string", + "enum": [ + "exact", + "best_effort", + "unconfirmed" + ] + } + }, + "required": [ + "platform", + "unit", + "confidence" + ] + }, + "previous_attributes": { + "type": "null" + } + }, + "required": [ + "id", + "object", + "type", + "api_version", + "created_at", + "idempotency_key", + "livemode", + "data", + "source", + "previous_attributes" + ] + }, + "PassVoidedEvent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "type": { + "type": "string", + "enum": [ + "pass.voided" + ] + }, + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "idempotency_key": { + "type": "string" + }, + "livemode": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "object": { + "type": "object", + "properties": { + "pass": { + "$ref": "#/components/schemas/EventPassSnapshot" + } + }, + "required": [ + "pass" + ] + } + }, + "required": [ + "object" + ] + }, + "source": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "unit": { + "type": [ + "string", + "null" + ], + "enum": [ + "device", + "object", + null + ] + }, + "confidence": { + "type": "string", + "enum": [ + "exact", + "best_effort", + "unconfirmed" + ] + } + }, + "required": [ + "platform", + "unit", + "confidence" + ] + }, + "previous_attributes": { + "type": "null" + } + }, + "required": [ + "id", + "object", + "type", + "api_version", + "created_at", + "idempotency_key", + "livemode", + "data", + "source", + "previous_attributes" + ] + }, + "PassRedeemedEvent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "type": { + "type": "string", + "enum": [ + "pass.redeemed" + ] + }, + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "idempotency_key": { + "type": "string" + }, + "livemode": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "object": { + "type": "object", + "properties": { + "pass": { + "$ref": "#/components/schemas/EventPassSnapshot" + }, + "redemption": { + "$ref": "#/components/schemas/EventRedemption" + } + }, + "required": [ + "pass", + "redemption" + ] + } + }, + "required": [ + "object" + ] + }, + "source": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "unit": { + "type": [ + "string", + "null" + ], + "enum": [ + "device", + "object", + null + ] + }, + "confidence": { + "type": "string", + "enum": [ + "exact", + "best_effort", + "unconfirmed" + ] + } + }, + "required": [ + "platform", + "unit", + "confidence" + ] + }, + "previous_attributes": { + "type": "null" + } + }, + "required": [ + "id", + "object", + "type", + "api_version", + "created_at", + "idempotency_key", + "livemode", + "data", + "source", + "previous_attributes" + ] + }, + "EventRedemption": { + "type": "object", + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "scanner_device_id": { + "type": [ + "string", + "null" + ] + }, + "scans_used": { + "type": [ + "number", + "null" + ] + }, + "policy_mode": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "scanner_device_id", + "scans_used", + "policy_mode" + ], + "description": "`data.object.redemption`, present on `pass.redeemed`." + }, + "ScannerPairing": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "scanner_pairing" + ] + }, + "token": { + "type": "string" + }, + "device": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "test", + "live" + ] + }, + "template_id": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "name", + "mode", + "template_id" + ] + } + }, + "required": [ + "object", + "token", + "device" + ] + }, + "Error": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "authentication_error", + "authorization_error", + "invalid_request_error", + "not_found_error", + "rate_limit_error", + "idempotency_error", + "api_error", + "billing_error" + ] + }, + "code": { + "type": "string", + "description": "A stable, machine-readable code when the error has a specific cause. Not exhaustive: treat unknown codes as the generic error for the `type`. Codes include `api_key_expired` (401, the key's expiry has passed), `insufficient_scope` (403, the key's scopes do not allow this route), `invalid_limit` (400, `limit` is not a positive integer within the maximum), `invalid_cursor` (400, `starting_after` is not an id on this list) and `plan_limit_exceeded` (402).", + "examples": [ + "api_key_expired", + "insufficient_scope", + "invalid_limit", + "invalid_cursor" + ] + }, + "message": { + "type": "string" + }, + "param": { + "type": "string" + } + }, + "required": [ + "type", + "message" + ] + } + }, + "required": [ + "error" + ] + }, + "ScanCodeRequest": { + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 512 + } + }, + "required": [ + "code" + ] + }, + "Account": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "account" + ] + }, + "organization_id": { + "type": "string" + }, + "organization_name": { + "type": "string" + }, + "organization_slug": { + "type": [ + "string", + "null" + ] + }, + "mode": { + "type": "string", + "enum": [ + "test", + "live" + ] + }, + "plan": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "usage": { + "type": "object", + "properties": { + "period_start": { + "type": [ + "string", + "null" + ] + }, + "api_calls": { + "type": "object", + "properties": { + "used": { + "type": "integer" + }, + "quota": { + "type": [ + "integer", + "null" + ] + }, + "grace_ceiling": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "used", + "quota", + "grace_ceiling" + ] + }, + "active_passes": { + "type": "object", + "properties": { + "used": { + "type": "integer" + }, + "included": { + "type": "integer" + }, + "ceiling": { + "type": "integer" + } + }, + "required": [ + "used", + "included", + "ceiling" + ] + } + }, + "required": [ + "period_start", + "api_calls", + "active_passes" + ], + "description": "Live-mode usage in the current billing period (a rolling 30 days on plans without one). Pass issuance stops at api_calls.quota; other live writes continue to api_calls.grace_ceiling." + }, + "credentials": { + "type": "object", + "properties": { + "apple_certificates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "pass_type_identifier": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": "string", + "enum": [ + "pending_certificate", + "active", + "revoked" + ] + }, + "expires_at": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "name", + "pass_type_identifier", + "status", + "expires_at" + ] + } + }, + "google_issuers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "issuer_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "active", + "revoked" + ] + } + }, + "required": [ + "id", + "name", + "issuer_id", + "status" + ] + } + } + }, + "required": [ + "apple_certificates", + "google_issuers" + ], + "description": "The organization's own signing credentials. Live passes are only signed with these, never with Passmint's shared test credentials." + } + }, + "required": [ + "object", + "organization_id", + "organization_name", + "organization_slug", + "mode", + "plan", + "usage", + "credentials" + ] + }, + "EventList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Event" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ] + }, + "Event": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "type": { + "type": "string", + "enum": [ + "pass.issued", + "pass.add_intent", + "pass.added_to_wallet", + "pass.update_pushed", + "pass.update_delivered", + "pass.update_failed", + "pass.removed", + "pass.voided", + "pass.redeemed" + ] + }, + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "idempotency_key": { + "type": "string" + }, + "livemode": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "object": { + "type": "object", + "properties": { + "pass": { + "$ref": "#/components/schemas/EventPassSnapshot" + }, + "redemption": { + "$ref": "#/components/schemas/EventRedemption" + }, + "delivery_failure": { + "$ref": "#/components/schemas/EventDeliveryFailure" + } + }, + "required": [ + "pass" + ] + } + }, + "required": [ + "object" + ] + }, + "source": { + "type": "object", + "properties": { + "platform": { + "type": [ + "string", + "null" + ], + "enum": [ + "apple", + "google", + null + ] + }, + "unit": { + "type": [ + "string", + "null" + ], + "enum": [ + "device", + "object", + null + ] + }, + "confidence": { + "type": "string", + "enum": [ + "exact", + "best_effort", + "unconfirmed" + ] + } + }, + "required": [ + "platform", + "unit", + "confidence" + ] + }, + "previous_attributes": { + "type": "null" + } + }, + "required": [ + "id", + "object", + "type", + "api_version", + "created_at", + "idempotency_key", + "livemode", + "data", + "source", + "previous_attributes" + ] + }, + "Funnel": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "funnel" + ] + }, + "group_by": { + "type": "string", + "enum": [ + "total", + "template", + "platform", + "date", + "mode" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FunnelSummary" + } + }, + "confidence": { + "$ref": "#/components/schemas/FunnelConfidence" + } + }, + "required": [ + "object", + "group_by", + "data", + "confidence" + ] + }, + "FunnelSummary": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "issued": { + "type": "number" + }, + "add_intent": { + "type": "number" + }, + "added": { + "type": "number" + }, + "active": { + "type": "number" + }, + "removed": { + "type": "number" + }, + "update_pushed": { + "type": "number" + }, + "update_delivered": { + "type": "number" + }, + "install_rate": { + "type": [ + "number", + "null" + ] + }, + "removal_rate": { + "type": [ + "number", + "null" + ] + }, + "update_delivery_rate": { + "type": [ + "number", + "null" + ] + } + }, + "required": [ + "key", + "issued", + "add_intent", + "added", + "active", + "removed", + "update_pushed", + "update_delivered", + "install_rate", + "removal_rate", + "update_delivery_rate" + ] + }, + "FunnelConfidence": { + "type": "object", + "properties": { + "removed": { + "type": "string" + }, + "update_delivery_rate": { + "type": "string" + }, + "active": { + "type": "string" + } + }, + "required": [ + "removed", + "update_delivery_rate", + "active" + ] + }, + "TemplateList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Template" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ] + }, + "Template": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "template" + ] + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "apple_style": { + "type": "string" + }, + "starter_template_id": { + "type": [ + "string", + "null" + ] + }, + "design": { + "$ref": "#/components/schemas/TemplateDesign" + }, + "archived": { + "type": "boolean" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "apple", + "google" + ] + } + }, + "certificate_set_id": { + "type": [ + "string", + "null" + ] + }, + "google_issuer_id": { + "type": [ + "string", + "null" + ] + }, + "require_download_link": { + "type": "boolean" + }, + "redemption_policy": { + "$ref": "#/components/schemas/RedemptionPolicy" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Present only when the saved design breaks a recommended rule (an Apple field limit, a duplicate field key, an unknown barcode placeholder, an image the pass style cannot show). The template is saved; fix these before they surprise you at issuance." + }, + "image_variants": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The template's image variant names that hold at least one image, sorted. A pass selects one with `image_variant`.", + "example": [ + "0", + "1", + "2", + "reward" + ] + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "object", + "name", + "type", + "apple_style", + "starter_template_id", + "design", + "archived", + "platforms", + "certificate_set_id", + "google_issuer_id", + "require_download_link", + "redemption_policy", + "image_variants", + "created_at", + "updated_at" + ] + }, + "TemplateDesign": { + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "logoText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "translations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "default" + ] + }, + { + "type": "null" + } + ] + }, + "foregroundColor": { + "type": "string" + }, + "backgroundColor": { + "type": "string" + }, + "labelColor": { + "type": "string" + }, + "headerFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "translations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "default" + ] + }, + { + "type": "null" + } + ] + }, + "defaultValue": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "textAlignment": { + "type": "string" + }, + "changeMessage": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "key" + ], + "additionalProperties": {} + } + }, + "primaryFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "translations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "default" + ] + }, + { + "type": "null" + } + ] + }, + "defaultValue": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "textAlignment": { + "type": "string" + }, + "changeMessage": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "key" + ], + "additionalProperties": {} + } + }, + "secondaryFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "translations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "default" + ] + }, + { + "type": "null" + } + ] + }, + "defaultValue": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "textAlignment": { + "type": "string" + }, + "changeMessage": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "key" + ], + "additionalProperties": {} + } + }, + "auxiliaryFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "translations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "default" + ] + }, + { + "type": "null" + } + ] + }, + "defaultValue": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "textAlignment": { + "type": "string" + }, + "changeMessage": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "key" + ], + "additionalProperties": {} + } + }, + "backFields": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "translations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "default" + ] + }, + { + "type": "null" + } + ] + }, + "defaultValue": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "textAlignment": { + "type": "string" + }, + "changeMessage": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "key" + ], + "additionalProperties": {} + } + }, + "barcodeFormat": { + "type": [ + "string", + "null" + ] + }, + "barcodeMessageTemplate": { + "type": [ + "string", + "null" + ] + }, + "issuerName": { + "type": [ + "string", + "null" + ] + }, + "locations": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "additionalProperties": {} + } + }, + "relevantDate": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": {}, + "description": "The template's design as stored. See CreateTemplateBody.design for the write contract." + }, + "RedemptionPolicy": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "single_use", + "reusable", + "count" + ] + }, + "maxUses": { + "type": "integer" + } + }, + "required": [ + "mode" + ] + }, + "DeletedTemplate": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "id", + "deleted" + ] + }, + "TemplateImage": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "template_image" + ] + }, + "template_id": { + "type": "string" + }, + "slot": { + "type": "string" + }, + "variant": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "object", + "template_id", + "slot", + "variant" + ] + }, + "DeletedTemplateImage": { + "allOf": [ + { + "$ref": "#/components/schemas/TemplateImage" + }, + { + "type": "object", + "properties": { + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "deleted" + ] + } + ] + }, + "WebhookList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Webhook" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ] + }, + "Webhook": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "webhook" + ] + }, + "url": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "enabled": { + "type": "boolean" + }, + "secret": { + "type": "string" + }, + "created_at": { + "type": "string" + } + }, + "required": [ + "id", + "object", + "url", + "events", + "description", + "enabled", + "created_at" + ] + }, + "DeletedWebhook": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "id", + "deleted" + ] + }, + "WebhookDeliveryList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookDelivery" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ] + }, + "WebhookDelivery": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "webhook_delivery" + ] + }, + "webhook_id": { + "type": "string" + }, + "event_type": { + "type": "string" + }, + "status": { + "type": "string" + }, + "attempts": { + "type": "number" + }, + "last_attempt_at": { + "type": [ + "string", + "null" + ] + }, + "next_attempt_at": { + "type": [ + "string", + "null" + ] + }, + "response_status": { + "type": [ + "number", + "null" + ] + }, + "response_body": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "string" + } + }, + "required": [ + "id", + "object", + "webhook_id", + "event_type", + "status", + "attempts", + "last_attempt_at", + "next_attempt_at", + "response_status", + "response_body", + "created_at" + ] + }, + "WebhookBackfill": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "backfill" + ] + }, + "enqueued": { + "type": "number" + }, + "capped": { + "type": "boolean" + }, + "window": { + "type": "object", + "properties": { + "since": { + "type": "string" + }, + "until": { + "type": "string" + } + }, + "required": [ + "since", + "until" + ] + } + }, + "required": [ + "object", + "enqueued", + "capped", + "window" + ] + }, + "PassList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pass" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ] + }, + "Pass": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "pass" + ] + }, + "short_id": { + "type": "string" + }, + "template_id": { + "type": "string" + }, + "certificate_set_id": { + "type": [ + "string", + "null" + ] + }, + "serial_number": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "holder_email": { + "type": [ + "string", + "null" + ] + }, + "holder_name": { + "type": [ + "string", + "null" + ] + }, + "field_values": { + "type": "object", + "additionalProperties": {}, + "description": "The pass's field values by field key. Normally strings; any JSON value sent is stored and returned as-is." + }, + "voided": { + "type": "boolean" + }, + "voided_at": { + "type": [ + "string", + "null" + ] + }, + "redeemed_count": { + "type": "integer" + }, + "first_redeemed_at": { + "type": [ + "string", + "null" + ] + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "Free-form key/value data you attached to the pass, or null." + }, + "image_variant": { + "type": [ + "string", + "null" + ], + "description": "The template image variant this pass renders with, or null for the template's own images." + }, + "images": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": [ + "pass", + "variant", + "template" + ] + } + }, + "required": [ + "source" + ] + }, + "description": "Every image slot the pass renders, keyed by Apple slot name, with the layer it comes from: `pass` (this pass's own image, `PUT /v1/passes/{id}/images/{slot}`), `variant` (its `image_variant`) or `template`. Carries no URLs.", + "example": { + "icon": { + "source": "template" + }, + "logo": { + "source": "template" + }, + "thumbnail": { + "source": "pass" + } + } + }, + "created_via_api": { + "type": "boolean" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "apple", + "google" + ] + }, + "description": "The wallets this pass was issued for." + }, + "platform_status": { + "type": [ + "object", + "null" + ], + "properties": { + "apple": { + "type": "string", + "enum": [ + "delivered", + "skipped_no_credential", + "failed" + ] + }, + "google": { + "type": "string", + "enum": [ + "delivered", + "skipped_no_credential", + "failed" + ] + } + }, + "description": "Per-wallet delivery outcome. A wallet is absent when the pass was not issued for it." + }, + "url": { + "type": "string" + }, + "download_url": { + "type": [ + "string", + "null" + ] + }, + "google_wallet_url": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "string" + } + }, + "required": [ + "id", + "object", + "short_id", + "template_id", + "certificate_set_id", + "serial_number", + "mode", + "holder_email", + "holder_name", + "field_values", + "voided", + "voided_at", + "redeemed_count", + "first_redeemed_at", + "metadata", + "image_variant", + "images", + "created_via_api", + "platforms", + "platform_status", + "url", + "download_url", + "google_wallet_url", + "created_at" + ] + }, + "PassCreateResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "pass" + ] + }, + "short_id": { + "type": "string" + }, + "template_id": { + "type": "string" + }, + "certificate_set_id": { + "type": [ + "string", + "null" + ] + }, + "serial_number": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "holder_email": { + "type": [ + "string", + "null" + ] + }, + "holder_name": { + "type": [ + "string", + "null" + ] + }, + "field_values": { + "type": "object", + "additionalProperties": {}, + "description": "The pass's field values by field key. Normally strings; any JSON value sent is stored and returned as-is." + }, + "voided": { + "type": "boolean" + }, + "voided_at": { + "type": [ + "string", + "null" + ] + }, + "redeemed_count": { + "type": "integer" + }, + "first_redeemed_at": { + "type": [ + "string", + "null" + ] + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "Free-form key/value data you attached to the pass, or null." + }, + "image_variant": { + "type": [ + "string", + "null" + ], + "description": "The template image variant this pass renders with, or null for the template's own images." + }, + "images": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": [ + "pass", + "variant", + "template" + ] + } + }, + "required": [ + "source" + ] + }, + "description": "Every image slot the pass renders, keyed by Apple slot name, with the layer it comes from: `pass` (this pass's own image, `PUT /v1/passes/{id}/images/{slot}`), `variant` (its `image_variant`) or `template`. Carries no URLs.", + "example": { + "icon": { + "source": "template" + }, + "logo": { + "source": "template" + }, + "thumbnail": { + "source": "pass" + } + } + }, + "created_via_api": { + "type": "boolean" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "apple", + "google" + ] + }, + "description": "The wallets this pass was issued for." + }, + "platform_status": { + "type": [ + "object", + "null" + ], + "properties": { + "apple": { + "type": "string", + "enum": [ + "delivered", + "skipped_no_credential", + "failed" + ] + }, + "google": { + "type": "string", + "enum": [ + "delivered", + "skipped_no_credential", + "failed" + ] + } + }, + "description": "Per-wallet delivery outcome. A wallet is absent when the pass was not issued for it." + }, + "url": { + "type": "string" + }, + "download_url": { + "type": [ + "string", + "null" + ] + }, + "google_wallet_url": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "object", + "short_id", + "template_id", + "certificate_set_id", + "serial_number", + "mode", + "holder_email", + "holder_name", + "field_values", + "voided", + "voided_at", + "redeemed_count", + "first_redeemed_at", + "metadata", + "image_variant", + "images", + "created_via_api", + "platforms", + "platform_status", + "url", + "download_url", + "google_wallet_url", + "created_at", + "warnings" + ] + }, + "PassUpdateResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "pass" + ] + }, + "short_id": { + "type": "string" + }, + "template_id": { + "type": "string" + }, + "certificate_set_id": { + "type": [ + "string", + "null" + ] + }, + "serial_number": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "holder_email": { + "type": [ + "string", + "null" + ] + }, + "holder_name": { + "type": [ + "string", + "null" + ] + }, + "field_values": { + "type": "object", + "additionalProperties": {}, + "description": "The pass's field values by field key. Normally strings; any JSON value sent is stored and returned as-is." + }, + "voided": { + "type": "boolean" + }, + "voided_at": { + "type": [ + "string", + "null" + ] + }, + "redeemed_count": { + "type": "integer" + }, + "first_redeemed_at": { + "type": [ + "string", + "null" + ] + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "Free-form key/value data you attached to the pass, or null." + }, + "image_variant": { + "type": [ + "string", + "null" + ], + "description": "The template image variant this pass renders with, or null for the template's own images." + }, + "images": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": [ + "pass", + "variant", + "template" + ] + } + }, + "required": [ + "source" + ] + }, + "description": "Every image slot the pass renders, keyed by Apple slot name, with the layer it comes from: `pass` (this pass's own image, `PUT /v1/passes/{id}/images/{slot}`), `variant` (its `image_variant`) or `template`. Carries no URLs.", + "example": { + "icon": { + "source": "template" + }, + "logo": { + "source": "template" + }, + "thumbnail": { + "source": "pass" + } + } + }, + "created_via_api": { + "type": "boolean" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "apple", + "google" + ] + }, + "description": "The wallets this pass was issued for." + }, + "platform_status": { + "type": [ + "object", + "null" + ], + "properties": { + "apple": { + "type": "string", + "enum": [ + "delivered", + "skipped_no_credential", + "failed" + ] + }, + "google": { + "type": "string", + "enum": [ + "delivered", + "skipped_no_credential", + "failed" + ] + } + }, + "description": "Per-wallet delivery outcome. A wallet is absent when the pass was not issued for it." + }, + "url": { + "type": "string" + }, + "download_url": { + "type": [ + "string", + "null" + ] + }, + "google_wallet_url": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "string" + }, + "delivery": { + "$ref": "#/components/schemas/PassDelivery" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "object", + "short_id", + "template_id", + "certificate_set_id", + "serial_number", + "mode", + "holder_email", + "holder_name", + "field_values", + "voided", + "voided_at", + "redeemed_count", + "first_redeemed_at", + "metadata", + "image_variant", + "images", + "created_via_api", + "platforms", + "platform_status", + "url", + "download_url", + "google_wallet_url", + "created_at", + "delivery", + "warnings" + ] + }, + "PassDelivery": { + "type": "object", + "properties": { + "apple": { + "type": [ + "object", + "null" + ], + "properties": { + "devices": { + "type": "integer" + }, + "pushed": { + "type": "integer" + }, + "failed": { + "type": "integer" + }, + "unregistered": { + "type": "integer" + } + }, + "required": [ + "devices", + "pushed", + "failed", + "unregistered" + ], + "description": "Null when the pass was not delivered to Apple Wallet. `failed` excludes dead tokens (`unregistered`), which mean the holder deleted the pass." + }, + "google": { + "type": [ + "object", + "null" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "updated", + "not_saved", + "failed", + "skipped" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status" + ], + "description": "Null when the pass has no Google Wallet object. `not_saved`: no holder has saved it yet. `skipped`: Google delivery was skipped at issue." + } + }, + "required": [ + "apple", + "google" + ] + }, + "PassVoidResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "pass" + ] + }, + "short_id": { + "type": "string" + }, + "template_id": { + "type": "string" + }, + "certificate_set_id": { + "type": [ + "string", + "null" + ] + }, + "serial_number": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "holder_email": { + "type": [ + "string", + "null" + ] + }, + "holder_name": { + "type": [ + "string", + "null" + ] + }, + "field_values": { + "type": "object", + "additionalProperties": {}, + "description": "The pass's field values by field key. Normally strings; any JSON value sent is stored and returned as-is." + }, + "voided": { + "type": "boolean" + }, + "voided_at": { + "type": [ + "string", + "null" + ] + }, + "redeemed_count": { + "type": "integer" + }, + "first_redeemed_at": { + "type": [ + "string", + "null" + ] + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "Free-form key/value data you attached to the pass, or null." + }, + "image_variant": { + "type": [ + "string", + "null" + ], + "description": "The template image variant this pass renders with, or null for the template's own images." + }, + "images": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": [ + "pass", + "variant", + "template" + ] + } + }, + "required": [ + "source" + ] + }, + "description": "Every image slot the pass renders, keyed by Apple slot name, with the layer it comes from: `pass` (this pass's own image, `PUT /v1/passes/{id}/images/{slot}`), `variant` (its `image_variant`) or `template`. Carries no URLs.", + "example": { + "icon": { + "source": "template" + }, + "logo": { + "source": "template" + }, + "thumbnail": { + "source": "pass" + } + } + }, + "created_via_api": { + "type": "boolean" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "apple", + "google" + ] + }, + "description": "The wallets this pass was issued for." + }, + "platform_status": { + "type": [ + "object", + "null" + ], + "properties": { + "apple": { + "type": "string", + "enum": [ + "delivered", + "skipped_no_credential", + "failed" + ] + }, + "google": { + "type": "string", + "enum": [ + "delivered", + "skipped_no_credential", + "failed" + ] + } + }, + "description": "Per-wallet delivery outcome. A wallet is absent when the pass was not issued for it." + }, + "url": { + "type": "string" + }, + "download_url": { + "type": [ + "string", + "null" + ] + }, + "google_wallet_url": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "object", + "short_id", + "template_id", + "certificate_set_id", + "serial_number", + "mode", + "holder_email", + "holder_name", + "field_values", + "voided", + "voided_at", + "redeemed_count", + "first_redeemed_at", + "metadata", + "image_variant", + "images", + "created_via_api", + "platforms", + "platform_status", + "url", + "download_url", + "google_wallet_url", + "created_at", + "warnings" + ] + }, + "PassEventList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PassEvent" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ] + }, + "PassEvent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "pass_event" + ] + }, + "pass_id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "Event-specific detail, such as the platform or a redemption's scanner. Shape varies by `type`; null when the event carries none." + }, + "created_at": { + "type": "string" + } + }, + "required": [ + "id", + "object", + "pass_id", + "type", + "metadata", + "created_at" + ] + }, + "RedemptionList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Redemption" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ] + }, + "Redemption": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "redemption" + ] + }, + "decision": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ] + }, + "reason": { + "type": [ + "string", + "null" + ], + "enum": [ + "already_redeemed", + "count_exhausted", + "voided", + "not_found", + "out_of_scope", + "wrong_mode", + null + ] + }, + "scanner_name": { + "type": [ + "string", + "null" + ] + }, + "scanned_at": { + "type": "string" + } + }, + "required": [ + "id", + "object", + "decision", + "reason", + "scanner_name", + "scanned_at" + ] + }, + "PassDownloadLink": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "pass.download_link" + ] + }, + "pass_id": { + "type": "string" + }, + "url": { + "type": "string" + }, + "expires_in": { + "type": "number" + }, + "expires_at": { + "type": "string" + }, + "created_at": { + "type": "string" + } + }, + "required": [ + "object", + "pass_id", + "url", + "expires_in", + "expires_at", + "created_at" + ] + }, + "PassImageResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "pass" + ] + }, + "short_id": { + "type": "string" + }, + "template_id": { + "type": "string" + }, + "certificate_set_id": { + "type": [ + "string", + "null" + ] + }, + "serial_number": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "holder_email": { + "type": [ + "string", + "null" + ] + }, + "holder_name": { + "type": [ + "string", + "null" + ] + }, + "field_values": { + "type": "object", + "additionalProperties": {}, + "description": "The pass's field values by field key. Normally strings; any JSON value sent is stored and returned as-is." + }, + "voided": { + "type": "boolean" + }, + "voided_at": { + "type": [ + "string", + "null" + ] + }, + "redeemed_count": { + "type": "integer" + }, + "first_redeemed_at": { + "type": [ + "string", + "null" + ] + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "Free-form key/value data you attached to the pass, or null." + }, + "image_variant": { + "type": [ + "string", + "null" + ], + "description": "The template image variant this pass renders with, or null for the template's own images." + }, + "images": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": [ + "pass", + "variant", + "template" + ] + } + }, + "required": [ + "source" + ] + }, + "description": "Every image slot the pass renders, keyed by Apple slot name, with the layer it comes from: `pass` (this pass's own image, `PUT /v1/passes/{id}/images/{slot}`), `variant` (its `image_variant`) or `template`. Carries no URLs.", + "example": { + "icon": { + "source": "template" + }, + "logo": { + "source": "template" + }, + "thumbnail": { + "source": "pass" + } + } + }, + "created_via_api": { + "type": "boolean" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "apple", + "google" + ] + }, + "description": "The wallets this pass was issued for." + }, + "platform_status": { + "type": [ + "object", + "null" + ], + "properties": { + "apple": { + "type": "string", + "enum": [ + "delivered", + "skipped_no_credential", + "failed" + ] + }, + "google": { + "type": "string", + "enum": [ + "delivered", + "skipped_no_credential", + "failed" + ] + } + }, + "description": "Per-wallet delivery outcome. A wallet is absent when the pass was not issued for it." + }, + "url": { + "type": "string" + }, + "download_url": { + "type": [ + "string", + "null" + ] + }, + "google_wallet_url": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "object", + "short_id", + "template_id", + "certificate_set_id", + "serial_number", + "mode", + "holder_email", + "holder_name", + "field_values", + "voided", + "voided_at", + "redeemed_count", + "first_redeemed_at", + "metadata", + "image_variant", + "images", + "created_via_api", + "platforms", + "platform_status", + "url", + "download_url", + "google_wallet_url", + "created_at", + "warnings" + ] + } + }, + "parameters": {} + }, + "paths": { + "/v1/scanners/pair": { + "post": { + "tags": [ + "Scanning" + ], + "summary": "Exchange a pairing code for a scanner token", + "description": "Unauthenticated: the single-use pairing code is the credential. Rate limited by client IP.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScanCodeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The scanner token, shown once", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScannerPairing" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limited", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/scan": { + "post": { + "tags": [ + "Scanning" + ], + "summary": "Scan a pass barcode", + "description": "A well-formed scan is always HTTP 200; the accept/reject outcome is in the body.", + "security": [ + { + "scannerToken": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScanCodeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The scan outcome", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "scan" + ] + } + }, + "required": [ + "object" + ], + "additionalProperties": {} + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limited", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/me": { + "get": { + "tags": [ + "Account" + ], + "summary": "Retrieve the authenticated account", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "The organization the API key belongs to", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/events": { + "get": { + "tags": [ + "Events" + ], + "summary": "List events", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": false, + "name": "type", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "pass_id", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "template_id", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "since", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "until", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "starting_after", + "in": "query" + }, + { + "schema": { + "type": [ + "number", + "null" + ] + }, + "required": false, + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A list of canonical events, most recent first", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventList" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/metrics/funnel": { + "get": { + "tags": [ + "Metrics" + ], + "summary": "Retrieve the pass funnel", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": false, + "name": "template_id", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "platform", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "since", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "until", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "group_by", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Funnel metrics for the authenticated organization", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Funnel" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/templates": { + "get": { + "tags": [ + "Templates" + ], + "summary": "List templates", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ], + "description": "Include archived templates. Defaults to false." + }, + "required": false, + "description": "Include archived templates. Defaults to false.", + "name": "include_archived", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Return items after this template id (the last item of the previous page)." + }, + "required": false, + "description": "Return items after this template id (the last item of the previous page).", + "name": "starting_after", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A list of the organization's templates — active only, unless include_archived=true", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateList" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Templates" + ], + "summary": "Create a template", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTemplateBody" + } + } + } + }, + "responses": { + "201": { + "description": "The created template", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Template" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "402": { + "description": "Plan limit reached", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Idempotency conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/templates/{id}": { + "get": { + "tags": [ + "Templates" + ], + "summary": "Retrieve a template", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The requested template", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Template" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Templates" + ], + "summary": "Update a template", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTemplateBody" + } + } + } + }, + "responses": { + "200": { + "description": "The updated template", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Template" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Templates" + ], + "summary": "Archive a template", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The template was archived", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedTemplate" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/templates/{id}/images/{slot}": { + "put": { + "tags": [ + "Templates" + ], + "summary": "Upload a template image", + "description": "Writes one image slot of the template — or, with `variant`, of one of its image variants. Passes pick up a change the next time they are issued or updated.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "An Apple image slot the template's style has: icon, logo, strip, thumbnail, background or footer (generic: icon, logo, thumbnail; storeCard and coupon: icon, logo, strip; eventTicket: icon, logo, strip, thumbnail, background; boardingPass: icon, logo, footer). With a variant, only the style's strip, thumbnail or background.", + "example": "strip" + }, + "required": true, + "description": "An Apple image slot the template's style has: icon, logo, strip, thumbnail, background or footer (generic: icon, logo, thumbnail; storeCard and coupon: icon, logo, strip; eventTicket: icon, logo, strip, thumbnail, background; boardingPass: icon, logo, footer). With a variant, only the style's strip, thumbnail or background.", + "name": "slot", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "string", + "description": "The image, base64-encoded: PNG or JPEG (WebP is also accepted). Any resolution — it is resized to the slot's 1x and @2x sizes. 8 MB limit." + }, + "variant": { + "type": [ + "string", + "null" + ], + "description": "Write this image variant's copy of the slot instead of the template's own. 1-32 characters: lowercase letters, digits, - and _, starting with a letter or digit.", + "example": "3" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The image was stored", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateImage" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Templates" + ], + "summary": "Delete a template image", + "description": "Removes one image slot of the template — or, with `?variant=`, of one of its image variants. A variant with no images left no longer appears in the template's `image_variants`. Any slot can be removed, including one the template's style does not use.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "An Apple image slot the template's style has: icon, logo, strip, thumbnail, background or footer (generic: icon, logo, thumbnail; storeCard and coupon: icon, logo, strip; eventTicket: icon, logo, strip, thumbnail, background; boardingPass: icon, logo, footer). With a variant, only the style's strip, thumbnail or background.", + "example": "strip" + }, + "required": true, + "description": "An Apple image slot the template's style has: icon, logo, strip, thumbnail, background or footer (generic: icon, logo, thumbnail; storeCard and coupon: icon, logo, strip; eventTicket: icon, logo, strip, thumbnail, background; boardingPass: icon, logo, footer). With a variant, only the style's strip, thumbnail or background.", + "name": "slot", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "Remove this image variant's copy of the slot. Omit it to remove the template's own." + }, + "required": false, + "description": "Remove this image variant's copy of the slot. Omit it to remove the template's own.", + "name": "variant", + "in": "query" + } + ], + "responses": { + "200": { + "description": "The image was removed (or was already absent)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedTemplateImage" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/webhooks": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "List webhook endpoints", + "description": "Every endpoint, newest first. Send limit or starting_after to page instead.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "100" + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Return items after this id (the last item of the previous page)." + }, + "required": false, + "description": "Return items after this id (the last item of the previous page).", + "name": "starting_after", + "in": "query" + } + ], + "responses": { + "200": { + "description": "The organization's webhook endpoints", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookList" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Webhooks" + ], + "summary": "Create a webhook endpoint", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookBody" + } + } + } + }, + "responses": { + "201": { + "description": "The created endpoint, including its signing secret (shown once)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Webhook" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "402": { + "description": "Plan limit reached", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/webhooks/{id}": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "Retrieve a webhook endpoint", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The webhook endpoint", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Webhook" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Webhooks" + ], + "summary": "Update a webhook endpoint", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookBody" + } + } + } + }, + "responses": { + "200": { + "description": "The updated webhook endpoint", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Webhook" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Webhooks" + ], + "summary": "Delete a webhook endpoint", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Confirmation the endpoint was deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedWebhook" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/webhooks/{id}/deliveries": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "List recent deliveries for a webhook endpoint", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "example": "100" + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Return items after this id (the last item of the previous page)." + }, + "required": false, + "description": "Return items after this id (the last item of the previous page).", + "name": "starting_after", + "in": "query" + } + ], + "responses": { + "200": { + "description": "The endpoint's most recent deliveries", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookDeliveryList" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/webhooks/{id}/deliveries/{deliveryId}/replay": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "Replay a webhook delivery", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "deliveryId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The delivery after being re-queued and flushed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookDelivery" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/webhooks/{id}/backfill": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "Backfill historical events to a webhook endpoint", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BackfillBody" + } + } + } + }, + "responses": { + "200": { + "description": "How many canonical deliveries were enqueued", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookBackfill" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/passes": { + "get": { + "tags": [ + "Passes" + ], + "summary": "List passes", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "tmpl_123" + }, + "required": false, + "name": "template_id", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "holder_email", + "in": "query" + }, + { + "schema": { + "type": "string", + "example": "50" + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Return items after this pass id (the last item of the previous page)." + }, + "required": false, + "description": "Return items after this pass id (the last item of the previous page).", + "name": "starting_after", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A list of passes for the authenticated organization", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassList" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Passes" + ], + "summary": "Create a pass", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "template_id": { + "type": "string" + }, + "certificate_set_id": { + "type": [ + "string", + "null" + ] + }, + "holder_email": { + "type": [ + "string", + "null" + ] + }, + "holder_name": { + "type": [ + "string", + "null" + ] + }, + "field_values": { + "type": "object", + "additionalProperties": {} + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {} + }, + "platforms": { + "type": "array", + "items": { + "type": "string" + } + }, + "image_variant": { + "type": [ + "string", + "null" + ], + "description": "Render the pass with this template image variant (see the template's `image_variants`). Omit or null for the template's own images." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "The created pass", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassCreateResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "402": { + "description": "Plan limit reached", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Idempotency conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/passes/{id}": { + "get": { + "tags": [ + "Passes" + ], + "summary": "Retrieve a pass", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "pass_123" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The requested pass", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pass" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Passes" + ], + "summary": "Update a pass", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "pass_123" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field_values": { + "type": "object", + "additionalProperties": {} + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {} + }, + "image_variant": { + "type": [ + "string", + "null" + ], + "description": "Switch the pass to this template image variant; null switches it back to the template's own images; omit to leave it unchanged. Re-signs and pushes like any other update." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The updated pass", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassUpdateResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Idempotency conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Passes" + ], + "summary": "Void a pass", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "pass_123" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The voided pass", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassVoidResponse" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/passes/{id}/events": { + "get": { + "tags": [ + "Passes" + ], + "summary": "List a pass's events", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "pass_123" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "example": "100" + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Return events after this event id (the last item of the previous page)." + }, + "required": false, + "description": "Return events after this event id (the last item of the previous page).", + "name": "starting_after", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A list of events for the pass", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassEventList" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/passes/{id}/redemptions": { + "get": { + "tags": [ + "Passes" + ], + "summary": "List a pass's scans", + "description": "Every scan of the pass, accepted or rejected, most recent first. A rejected scan carries its reason and did not change the pass.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "pass_123" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "example": "100" + }, + "required": false, + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "The pass's scan log", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RedemptionList" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/passes/{id}/download-links": { + "post": { + "tags": [ + "Passes" + ], + "summary": "Create an expiring download link", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "pass_123" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "expires_in": { + "type": "number" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "A tokenized link to the pass page that stops resolving after expires_in seconds", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassDownloadLink" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/passes/{id}/images/{slot}": { + "put": { + "tags": [ + "Passes" + ], + "summary": "Upload a pass image", + "description": "Sets this pass's own image of one slot, over its image variant's and its template's. The pass is re-rendered, re-signed and pushed to installed devices, like any update. On Google Wallet a pass strip is the object's hero image; a pass thumbnail is the object's logo on generic passes and an image module on every other type; background is Apple only. Replacing an image retires the public URL Google fetched the old one from. A voided pass is a 409 (pass_voided). If the Google Wallet object could not be updated, the request still succeeds and `warnings` says why.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "pass_123" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "The image slot, by its Apple name: strip, thumbnail or background — whichever of those the template's style has (storeCard and coupon: strip; generic: thumbnail; eventTicket: strip, thumbnail, background; boardingPass: none).", + "example": "thumbnail" + }, + "required": true, + "description": "The image slot, by its Apple name: strip, thumbnail or background — whichever of those the template's style has (storeCard and coupon: strip; generic: thumbnail; eventTicket: strip, thumbnail, background; boardingPass: none).", + "name": "slot", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "string", + "description": "The image, base64-encoded: PNG or JPEG (WebP is also accepted). Any resolution — it is resized to the slot's 1x and @2x sizes. 8 MB limit." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The updated pass, with any non-fatal warnings", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassImageResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Idempotency conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Passes" + ], + "summary": "Delete a pass image", + "description": "Removes this pass's own image of one slot, so the slot falls back to the pass's image variant or its template. The stored image is deleted and its public URL stops resolving: this is also how to erase a pass image (a member photo, say), and it works on a voided pass (which is not re-rendered). Otherwise the pass is re-rendered and pushed. Deleting a slot the pass has no image of changes nothing and returns the pass.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "pass_123" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "The image slot, by its Apple name: strip, thumbnail or background — whichever of those the template's style has (storeCard and coupon: strip; generic: thumbnail; eventTicket: strip, thumbnail, background; boardingPass: none).", + "example": "thumbnail" + }, + "required": true, + "description": "The image slot, by its Apple name: strip, thumbnail or background — whichever of those the template's style has (storeCard and coupon: strip; generic: thumbnail; eventTicket: strip, thumbnail, background; boardingPass: none).", + "name": "slot", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The updated pass, with any non-fatal warnings", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassImageResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Authentication failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (API key scope, plan or account)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Idempotency conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "webhooks": { + "pass.issued": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "pass.issued event", + "description": "Sent to your webhook endpoint when a `pass.issued` event occurs. Verify the signature, then return any 2xx to acknowledge.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassIssuedEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx to acknowledge" + } + } + } + }, + "pass.add_intent": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "pass.add_intent event", + "description": "Sent to your webhook endpoint when a `pass.add_intent` event occurs. Verify the signature, then return any 2xx to acknowledge.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassAddIntentEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx to acknowledge" + } + } + } + }, + "pass.added_to_wallet": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "pass.added_to_wallet event", + "description": "Sent to your webhook endpoint when a `pass.added_to_wallet` event occurs. Verify the signature, then return any 2xx to acknowledge.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassAddedToWalletEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx to acknowledge" + } + } + } + }, + "pass.update_pushed": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "pass.update_pushed event", + "description": "Sent to your webhook endpoint when a `pass.update_pushed` event occurs. Verify the signature, then return any 2xx to acknowledge.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassUpdatePushedEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx to acknowledge" + } + } + } + }, + "pass.update_delivered": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "pass.update_delivered event", + "description": "Sent to your webhook endpoint when a `pass.update_delivered` event occurs. Verify the signature, then return any 2xx to acknowledge.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassUpdateDeliveredEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx to acknowledge" + } + } + } + }, + "pass.update_failed": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "pass.update_failed event", + "description": "Sent to your webhook endpoint when a `pass.update_failed` event occurs. Verify the signature, then return any 2xx to acknowledge.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassUpdateFailedEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx to acknowledge" + } + } + } + }, + "pass.removed": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "pass.removed event", + "description": "Sent to your webhook endpoint when a `pass.removed` event occurs. Verify the signature, then return any 2xx to acknowledge.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassRemovedEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx to acknowledge" + } + } + } + }, + "pass.voided": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "pass.voided event", + "description": "Sent to your webhook endpoint when a `pass.voided` event occurs. Verify the signature, then return any 2xx to acknowledge.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassVoidedEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx to acknowledge" + } + } + } + }, + "pass.redeemed": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "pass.redeemed event", + "description": "Sent to your webhook endpoint when a `pass.redeemed` event occurs. Verify the signature, then return any 2xx to acknowledge.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassRedeemedEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx to acknowledge" + } + } + } + } + } +} diff --git a/package.json b/package.json index c8d646e..a031081 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,17 @@ "clean": "rm -rf dist .turbo", "changeset": "changeset", "version-packages": "changeset version && node scripts/sync-version.mjs && pnpm format", - "release": "changeset publish" + "release": "changeset publish", + "sync-spec": "node scripts/sync-spec.mjs", + "generate": "openapi-typescript openapi.json -o src/generated/openapi.ts && node scripts/gen-event-types.mjs", + "generate:check": "pnpm generate && git diff --exit-code -- src/generated" }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.2", "@biomejs/biome": "^1.9.4", "@changesets/cli": "^2.27.11", "@types/node": "^22.10.5", + "openapi-typescript": "7.13.0", "publint": "^0.3.2", "tsdown": "^0.10.1", "typescript": "^5.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71ca30c..448f1f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@types/node': specifier: ^22.10.5 version: 22.19.17 + openapi-typescript: + specifier: 7.13.0 + version: 7.13.0(typescript@5.9.3) publint: specifier: ^0.3.2 version: 0.3.18 @@ -47,6 +50,10 @@ packages: resolution: {integrity: sha512-Izvir8iIoU+X4SKtDAa5kpb+9cpifclzsbA8x/AZY0k0gIfXYQ1fa1B6Epfe6vNA2YfDX8VtrZFgvnXB6aPEoQ==} engines: {node: '>=18'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} @@ -59,6 +66,10 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.2': resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} @@ -517,6 +528,16 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} + + '@redocly/openapi-core@1.34.20': + resolution: {integrity: sha512-ypeBZ/6BKXR9+7/TtbKhbl4UgD7raHhPS12oknlKno2A8+lnFkxIwiE/Aklu6L2cd/ioH+fCWuMxi9/p3EyAPw==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@rolldown/binding-darwin-arm64@1.0.0-beta.8-commit.852c603': resolution: {integrity: sha512-V0zbNPcyRVZFUUtCiJxXMuOYriszcgzc5wW5DUA5mHDlM0gDc1snsLzM245q5fGlSLmq+Rgerkclv0ZEjgHwDw==} cpu: [arm64] @@ -757,6 +778,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -802,10 +827,16 @@ packages: resolution: {integrity: sha512-MdJqjpodkS5J149zN0Po+HPshkTdUyrvF7CKTafUgv69vBSPtncrj+3IiUgqdd7ElIEkbeXCsEouBUwLrw9Ilg==} engines: {node: '>=16.14.0'} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + brace-expansion@2.1.7: + resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -826,6 +857,9 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -863,6 +897,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -956,6 +993,9 @@ packages: extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -1024,6 +1064,10 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -1036,6 +1080,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1067,6 +1115,13 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -1075,11 +1130,18 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -1188,6 +1250,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -1211,6 +1277,12 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -1247,6 +1319,10 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + parse5-htmlparser2-tree-adapter@6.0.1: resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} @@ -1293,6 +1369,10 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + postcss@8.5.9: resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} @@ -1328,6 +1408,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1429,6 +1513,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1494,6 +1582,10 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typescript@5.6.1-rc: resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} engines: {node: '>=14.17'} @@ -1529,6 +1621,9 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + valibot@1.0.0: resolution: {integrity: sha512-1Hc0ihzWxBar6NGeZv7fPLY0QuxFMyxwYR2sF1Blu7Wq7EnremwY2W02tit2ij2VJT8HcSkHAQqmFfl77f73Yw==} peerDependencies: @@ -1623,10 +1718,17 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} @@ -1656,6 +1758,12 @@ snapshots: typescript: 5.6.1-rc validate-npm-package-name: 5.0.1 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.2 @@ -1668,6 +1776,8 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/parser@7.29.2': dependencies: '@babel/types': 7.29.0 @@ -2093,6 +2203,29 @@ snapshots: dependencies: quansync: 1.0.0 + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.0': {} + + '@redocly/openapi-core@1.34.20(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.3.2 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + '@rolldown/binding-darwin-arm64@1.0.0-beta.8-commit.852c603': optional: true @@ -2267,6 +2400,8 @@ snapshots: acorn@8.16.0: {} + agent-base@7.1.4: {} + ansi-colors@4.1.3: {} ansi-escapes@7.3.0: @@ -2300,10 +2435,16 @@ snapshots: '@babel/parser': 7.29.2 pathe: 2.0.3 + balanced-match@1.0.2: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 + brace-expansion@2.1.7: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -2325,6 +2466,8 @@ snapshots: chalk@5.6.2: {} + change-case@5.4.4: {} + char-regex@1.0.2: {} chardet@2.1.1: {} @@ -2364,6 +2507,8 @@ snapshots: color-name@1.1.4: {} + colorette@1.4.0: {} + commander@10.0.1: {} consola@3.4.2: {} @@ -2374,9 +2519,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@4.4.3: + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 deep-eql@5.0.2: {} @@ -2450,6 +2597,8 @@ snapshots: extendable-error@0.1.7: {} + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2519,6 +2668,13 @@ snapshots: hookable@5.5.3: {} + https-proxy-agent@7.0.6(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + human-id@4.1.3: {} iconv-lite@0.7.2: @@ -2527,6 +2683,8 @@ snapshots: ignore@5.3.2: {} + index-to-position@1.2.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -2547,6 +2705,10 @@ snapshots: jiti@2.6.1: {} + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -2556,8 +2718,14 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} + json-schema-traverse@1.0.0: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -2645,6 +2813,10 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.7 + mri@1.2.0: {} ms@2.1.3: {} @@ -2666,6 +2838,16 @@ snapshots: object-assign@4.1.1: {} + openapi-typescript@7.13.0(typescript@5.9.3): + dependencies: + '@redocly/openapi-core': 1.34.20(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 5.9.3 + yargs-parser: 21.1.1 + outdent@0.5.0: {} oxc-resolver@9.0.2: @@ -2719,6 +2901,12 @@ snapshots: package-manager-detector@1.6.0: {} + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.7 + index-to-position: 1.2.0 + type-fest: 4.41.0 + parse5-htmlparser2-tree-adapter@6.0.1: dependencies: parse5: 6.0.1 @@ -2747,6 +2935,8 @@ snapshots: pify@4.0.1: {} + pluralize@8.0.0: {} + postcss@8.5.9: dependencies: nanoid: 3.3.11 @@ -2779,6 +2969,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -2791,7 +2983,7 @@ snapshots: '@babel/parser': 7.29.2 '@babel/types': 7.29.0 ast-kit: 1.4.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) dts-resolver: 1.2.0 get-tsconfig: 4.14.0 oxc-transform: 0.67.0 @@ -2907,6 +3099,8 @@ snapshots: strip-bom@3.0.0: {} + supports-color@10.2.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -2953,7 +3147,7 @@ snapshots: cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) diff: 7.0.0 empathic: 1.1.0 hookable: 5.5.3 @@ -2974,6 +3168,8 @@ snapshots: tslib@2.8.1: optional: true + type-fest@4.41.0: {} + typescript@5.6.1-rc: {} typescript@5.9.3: {} @@ -3010,6 +3206,8 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 + uri-js-replace@1.0.1: {} + valibot@1.0.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -3019,7 +3217,7 @@ snapshots: vite-node@2.1.9(@types/node@22.19.17)(lightningcss@1.32.0): dependencies: cac: 6.7.14 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 1.1.2 vite: 5.4.21(@types/node@22.19.17)(lightningcss@1.32.0) @@ -3054,7 +3252,7 @@ snapshots: '@vitest/spy': 2.1.9 '@vitest/utils': 2.1.9 chai: 5.3.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) expect-type: 1.3.0 magic-string: 0.30.21 pathe: 1.1.2 @@ -3098,8 +3296,12 @@ snapshots: y18n@5.0.8: {} + yaml-ast-parser@0.0.43: {} + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs@16.2.0: dependencies: cliui: 7.0.4 diff --git a/scripts/gen-event-types.mjs b/scripts/gen-event-types.mjs new file mode 100644 index 0000000..7d0fdf9 --- /dev/null +++ b/scripts/gen-event-types.mjs @@ -0,0 +1,22 @@ +// Writes src/generated/event-types.ts from the webhook keys in openapi.json. +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const spec = JSON.parse(readFileSync(resolve(root, 'openapi.json'), 'utf8')) +const types = Object.keys(spec.webhooks ?? {}) + +const body = `// Generated from openapi.json by scripts/gen-event-types.mjs. Do not edit. + +export const PASSMINT_EVENT_TYPES = [ +${types.map((t) => ` ${JSON.stringify(t)},`).join('\n')} +] as const + +export type PassmintEventType = (typeof PASSMINT_EVENT_TYPES)[number] +` + +const out = resolve(root, 'src/generated/event-types.ts') +mkdirSync(dirname(out), { recursive: true }) +writeFileSync(out, body) +console.log(`Wrote ${out}`) diff --git a/scripts/sync-spec.mjs b/scripts/sync-spec.mjs new file mode 100644 index 0000000..b91d822 --- /dev/null +++ b/scripts/sync-spec.mjs @@ -0,0 +1,22 @@ +// Copies the committed OpenAPI spec into this repo. +// Default: the sibling platform checkout (../platform). Override with PASSMINT_PLATFORM_DIR. +// Pass --from-url to fetch the live spec instead. +import { readFileSync, writeFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const target = resolve(root, 'openapi.json') + +let raw +if (process.argv.includes('--from-url')) { + const res = await fetch('https://api.passmint.com/openapi.json') + if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status}`) + raw = await res.text() +} else { + const platformDir = process.env.PASSMINT_PLATFORM_DIR ?? resolve(root, '../platform') + raw = readFileSync(resolve(platformDir, 'apps/api-worker/openapi.json'), 'utf8') +} + +writeFileSync(target, `${JSON.stringify(JSON.parse(raw), null, 2)}\n`) +console.log(`Wrote ${target}`) diff --git a/src/generated/event-types.ts b/src/generated/event-types.ts new file mode 100644 index 0000000..72aaa10 --- /dev/null +++ b/src/generated/event-types.ts @@ -0,0 +1,15 @@ +// Generated from openapi.json by scripts/gen-event-types.mjs. Do not edit. + +export const PASSMINT_EVENT_TYPES = [ + "pass.issued", + "pass.add_intent", + "pass.added_to_wallet", + "pass.update_pushed", + "pass.update_delivered", + "pass.update_failed", + "pass.removed", + "pass.voided", + "pass.redeemed", +] as const + +export type PassmintEventType = (typeof PASSMINT_EVENT_TYPES)[number] diff --git a/src/generated/openapi.ts b/src/generated/openapi.ts new file mode 100644 index 0000000..71b9636 --- /dev/null +++ b/src/generated/openapi.ts @@ -0,0 +1,3950 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/v1/scanners/pair": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Exchange a pairing code for a scanner token + * @description Unauthenticated: the single-use pairing code is the credential. Rate limited by client IP. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["ScanCodeRequest"]; + }; + }; + responses: { + /** @description The scanner token, shown once */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ScannerPairing"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Rate limited */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/scan": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Scan a pass barcode + * @description A well-formed scan is always HTTP 200; the accept/reject outcome is in the body. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["ScanCodeRequest"]; + }; + }; + responses: { + /** @description The scan outcome */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + object: "scan"; + } & { + [key: string]: unknown; + }; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Rate limited */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Retrieve the authenticated account */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The organization the API key belongs to */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Account"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List events */ + get: { + parameters: { + query?: { + type?: string; + pass_id?: string; + template_id?: string; + since?: string; + until?: string; + starting_after?: string; + limit?: number | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A list of canonical events, most recent first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventList"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/metrics/funnel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Retrieve the pass funnel */ + get: { + parameters: { + query?: { + template_id?: string; + platform?: string; + since?: string; + until?: string; + group_by?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Funnel metrics for the authenticated organization */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Funnel"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/templates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List templates */ + get: { + parameters: { + query?: { + limit?: string; + /** @description Include archived templates. Defaults to false. */ + include_archived?: "true" | "false"; + /** @description Return items after this template id (the last item of the previous page). */ + starting_after?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A list of the organization's templates — active only, unless include_archived=true */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateList"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + /** Create a template */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateTemplateBody"]; + }; + }; + responses: { + /** @description The created template */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Template"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Plan limit reached */ + 402: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Idempotency conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/templates/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Retrieve a template */ + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The requested template */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Template"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + /** Archive a template */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The template was archived */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletedTemplate"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + options?: never; + head?: never; + /** Update a template */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UpdateTemplateBody"]; + }; + }; + responses: { + /** @description The updated template */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Template"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + trace?: never; + }; + "/v1/templates/{id}/images/{slot}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Upload a template image + * @description Writes one image slot of the template — or, with `variant`, of one of its image variants. Passes pick up a change the next time they are issued or updated. + */ + put: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + /** @description An Apple image slot the template's style has: icon, logo, strip, thumbnail, background or footer (generic: icon, logo, thumbnail; storeCard and coupon: icon, logo, strip; eventTicket: icon, logo, strip, thumbnail, background; boardingPass: icon, logo, footer). With a variant, only the style's strip, thumbnail or background. */ + slot: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The image, base64-encoded: PNG or JPEG (WebP is also accepted). Any resolution — it is resized to the slot's 1x and @2x sizes. 8 MB limit. */ + data?: string; + /** + * @description Write this image variant's copy of the slot instead of the template's own. 1-32 characters: lowercase letters, digits, - and _, starting with a letter or digit. + * @example 3 + */ + variant?: string | null; + }; + }; + }; + responses: { + /** @description The image was stored */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateImage"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + post?: never; + /** + * Delete a template image + * @description Removes one image slot of the template — or, with `?variant=`, of one of its image variants. A variant with no images left no longer appears in the template's `image_variants`. Any slot can be removed, including one the template's style does not use. + */ + delete: { + parameters: { + query?: { + /** @description Remove this image variant's copy of the slot. Omit it to remove the template's own. */ + variant?: string; + }; + header?: never; + path: { + id: string; + /** @description An Apple image slot the template's style has: icon, logo, strip, thumbnail, background or footer (generic: icon, logo, thumbnail; storeCard and coupon: icon, logo, strip; eventTicket: icon, logo, strip, thumbnail, background; boardingPass: icon, logo, footer). With a variant, only the style's strip, thumbnail or background. */ + slot: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The image was removed (or was already absent) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletedTemplateImage"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/webhooks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List webhook endpoints + * @description Every endpoint, newest first. Send limit or starting_after to page instead. + */ + get: { + parameters: { + query?: { + limit?: string; + /** @description Return items after this id (the last item of the previous page). */ + starting_after?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The organization's webhook endpoints */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookList"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + /** Create a webhook endpoint */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateWebhookBody"]; + }; + }; + responses: { + /** @description The created endpoint, including its signing secret (shown once) */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Webhook"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Plan limit reached */ + 402: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/webhooks/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Retrieve a webhook endpoint */ + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The webhook endpoint */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Webhook"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + /** Delete a webhook endpoint */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Confirmation the endpoint was deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletedWebhook"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + options?: never; + head?: never; + /** Update a webhook endpoint */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UpdateWebhookBody"]; + }; + }; + responses: { + /** @description The updated webhook endpoint */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Webhook"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + trace?: never; + }; + "/v1/webhooks/{id}/deliveries": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List recent deliveries for a webhook endpoint */ + get: { + parameters: { + query?: { + limit?: string; + /** @description Return items after this id (the last item of the previous page). */ + starting_after?: string; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The endpoint's most recent deliveries */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookDeliveryList"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/webhooks/{id}/deliveries/{deliveryId}/replay": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Replay a webhook delivery */ + post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + deliveryId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The delivery after being re-queued and flushed */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookDelivery"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/webhooks/{id}/backfill": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Backfill historical events to a webhook endpoint */ + post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BackfillBody"]; + }; + }; + responses: { + /** @description How many canonical deliveries were enqueued */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookBackfill"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/passes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List passes */ + get: { + parameters: { + query?: { + template_id?: string; + holder_email?: string; + limit?: string; + /** @description Return items after this pass id (the last item of the previous page). */ + starting_after?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A list of passes for the authenticated organization */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PassList"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + /** Create a pass */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + template_id?: string; + certificate_set_id?: string | null; + holder_email?: string | null; + holder_name?: string | null; + field_values?: { + [key: string]: unknown; + }; + metadata?: { + [key: string]: unknown; + } | null; + platforms?: string[]; + /** @description Render the pass with this template image variant (see the template's `image_variants`). Omit or null for the template's own images. */ + image_variant?: string | null; + }; + }; + }; + responses: { + /** @description The created pass */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PassCreateResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Plan limit reached */ + 402: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Idempotency conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/passes/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Retrieve a pass */ + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The requested pass */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Pass"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + /** Void a pass */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The voided pass */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PassVoidResponse"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + options?: never; + head?: never; + /** Update a pass */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + field_values?: { + [key: string]: unknown; + }; + metadata?: { + [key: string]: unknown; + } | null; + /** @description Switch the pass to this template image variant; null switches it back to the template's own images; omit to leave it unchanged. Re-signs and pushes like any other update. */ + image_variant?: string | null; + }; + }; + }; + responses: { + /** @description The updated pass */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PassUpdateResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Idempotency conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + trace?: never; + }; + "/v1/passes/{id}/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List a pass's events */ + get: { + parameters: { + query?: { + limit?: string; + /** @description Return events after this event id (the last item of the previous page). */ + starting_after?: string; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A list of events for the pass */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PassEventList"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/passes/{id}/redemptions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List a pass's scans + * @description Every scan of the pass, accepted or rejected, most recent first. A rejected scan carries its reason and did not change the pass. + */ + get: { + parameters: { + query?: { + limit?: string; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The pass's scan log */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RedemptionList"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/passes/{id}/download-links": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create an expiring download link */ + post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + expires_in?: number; + }; + }; + }; + responses: { + /** @description A tokenized link to the pass page that stops resolving after expires_in seconds */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PassDownloadLink"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/passes/{id}/images/{slot}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Upload a pass image + * @description Sets this pass's own image of one slot, over its image variant's and its template's. The pass is re-rendered, re-signed and pushed to installed devices, like any update. On Google Wallet a pass strip is the object's hero image; a pass thumbnail is the object's logo on generic passes and an image module on every other type; background is Apple only. Replacing an image retires the public URL Google fetched the old one from. A voided pass is a 409 (pass_voided). If the Google Wallet object could not be updated, the request still succeeds and `warnings` says why. + */ + put: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + /** @description The image slot, by its Apple name: strip, thumbnail or background — whichever of those the template's style has (storeCard and coupon: strip; generic: thumbnail; eventTicket: strip, thumbnail, background; boardingPass: none). */ + slot: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The image, base64-encoded: PNG or JPEG (WebP is also accepted). Any resolution — it is resized to the slot's 1x and @2x sizes. 8 MB limit. */ + data?: string; + }; + }; + }; + responses: { + /** @description The updated pass, with any non-fatal warnings */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PassImageResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Idempotency conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + post?: never; + /** + * Delete a pass image + * @description Removes this pass's own image of one slot, so the slot falls back to the pass's image variant or its template. The stored image is deleted and its public URL stops resolving: this is also how to erase a pass image (a member photo, say), and it works on a voided pass (which is not re-rendered). Otherwise the pass is re-rendered and pushed. Deleting a slot the pass has no image of changes nothing and returns the pass. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + /** @description The image slot, by its Apple name: strip, thumbnail or background — whichever of those the template's style has (storeCard and coupon: strip; generic: thumbnail; eventTicket: strip, thumbnail, background; boardingPass: none). */ + slot: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The updated pass, with any non-fatal warnings */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PassImageResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden (API key scope, plan or account) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Idempotency conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export interface webhooks { + "pass.issued": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * pass.issued event + * @description Sent to your webhook endpoint when a `pass.issued` event occurs. Verify the signature, then return any 2xx to acknowledge. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PassIssuedEvent"]; + }; + }; + responses: { + /** @description Return any 2xx to acknowledge */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "pass.add_intent": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * pass.add_intent event + * @description Sent to your webhook endpoint when a `pass.add_intent` event occurs. Verify the signature, then return any 2xx to acknowledge. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PassAddIntentEvent"]; + }; + }; + responses: { + /** @description Return any 2xx to acknowledge */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "pass.added_to_wallet": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * pass.added_to_wallet event + * @description Sent to your webhook endpoint when a `pass.added_to_wallet` event occurs. Verify the signature, then return any 2xx to acknowledge. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PassAddedToWalletEvent"]; + }; + }; + responses: { + /** @description Return any 2xx to acknowledge */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "pass.update_pushed": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * pass.update_pushed event + * @description Sent to your webhook endpoint when a `pass.update_pushed` event occurs. Verify the signature, then return any 2xx to acknowledge. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PassUpdatePushedEvent"]; + }; + }; + responses: { + /** @description Return any 2xx to acknowledge */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "pass.update_delivered": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * pass.update_delivered event + * @description Sent to your webhook endpoint when a `pass.update_delivered` event occurs. Verify the signature, then return any 2xx to acknowledge. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PassUpdateDeliveredEvent"]; + }; + }; + responses: { + /** @description Return any 2xx to acknowledge */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "pass.update_failed": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * pass.update_failed event + * @description Sent to your webhook endpoint when a `pass.update_failed` event occurs. Verify the signature, then return any 2xx to acknowledge. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PassUpdateFailedEvent"]; + }; + }; + responses: { + /** @description Return any 2xx to acknowledge */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "pass.removed": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * pass.removed event + * @description Sent to your webhook endpoint when a `pass.removed` event occurs. Verify the signature, then return any 2xx to acknowledge. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PassRemovedEvent"]; + }; + }; + responses: { + /** @description Return any 2xx to acknowledge */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "pass.voided": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * pass.voided event + * @description Sent to your webhook endpoint when a `pass.voided` event occurs. Verify the signature, then return any 2xx to acknowledge. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PassVoidedEvent"]; + }; + }; + responses: { + /** @description Return any 2xx to acknowledge */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "pass.redeemed": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * pass.redeemed event + * @description Sent to your webhook endpoint when a `pass.redeemed` event occurs. Verify the signature, then return any 2xx to acknowledge. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PassRedeemedEvent"]; + }; + }; + responses: { + /** @description Return any 2xx to acknowledge */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export interface components { + schemas: { + CreateTemplateBody: { + name: string; + /** @enum {string} */ + type: "event" | "membership" | "coupon" | "loyalty" | "generic"; + /** @enum {string} */ + apple_style: "generic" | "eventTicket" | "boardingPass" | "coupon" | "storeCard"; + design: { + description?: string | null; + logoText?: string | null; + foregroundColor: string; + backgroundColor: string; + labelColor: string; + /** @description Storage key from an image upload. Leave null to keep none. */ + logoImageKey?: string | null; + /** @description Storage key from an image upload. Leave null to keep none. */ + iconImageKey?: string | null; + /** @description Storage key from an image upload. Leave null to keep none. */ + stripImageKey?: string | null; + /** @description Storage key from an image upload. Leave null to keep none. */ + thumbnailImageKey?: string | null; + headerFields?: { + /** @description Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design. */ + key: string; + label?: string | null; + defaultValue?: string | null; + /** @enum {string|null} */ + textAlignment?: "left" | "center" | "right" | "natural" | null; + required?: unknown; + /** @description Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder. */ + changeMessage?: string | null; + }[] | null; + primaryFields?: { + /** @description Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design. */ + key: string; + label?: string | null; + defaultValue?: string | null; + /** @enum {string|null} */ + textAlignment?: "left" | "center" | "right" | "natural" | null; + required?: unknown; + /** @description Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder. */ + changeMessage?: string | null; + }[] | null; + secondaryFields?: { + /** @description Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design. */ + key: string; + label?: string | null; + defaultValue?: string | null; + /** @enum {string|null} */ + textAlignment?: "left" | "center" | "right" | "natural" | null; + required?: unknown; + /** @description Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder. */ + changeMessage?: string | null; + }[] | null; + auxiliaryFields?: { + /** @description Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design. */ + key: string; + label?: string | null; + defaultValue?: string | null; + /** @enum {string|null} */ + textAlignment?: "left" | "center" | "right" | "natural" | null; + required?: unknown; + /** @description Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder. */ + changeMessage?: string | null; + }[] | null; + backFields?: { + /** @description Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design. */ + key: string; + label?: string | null; + defaultValue?: string | null; + /** @enum {string|null} */ + textAlignment?: "left" | "center" | "right" | "natural" | null; + required?: unknown; + /** @description Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder. */ + changeMessage?: string | null; + }[] | null; + /** @enum {string|null} */ + barcodeFormat?: "PKBarcodeFormatQR" | "PKBarcodeFormatPDF417" | "PKBarcodeFormatAztec" | "PKBarcodeFormatCode128" | null; + /** @description What the barcode encodes. Use '{{serialNumber}}' — scanning looks passes up by serial number, and it is unique per pass. '{{fieldValues.}}' inserts a field value. No other placeholders exist. */ + barcodeMessageTemplate?: string | null; + /** @description The issuer shown on the pass and in its notifications (Apple organizationName; Google issuer/program name unless logoText is set). Defaults to your organization's name. */ + issuerName?: string | null; + /** @description Up to 10 places the pass is relevant near. Apple Wallet suggests the pass on the lock screen there; Google Wallet stores them on the pass. */ + locations?: { + latitude: number; + longitude: number; + altitude?: number; + /** @description Lock-screen text Apple Wallet shows near this location. */ + relevantText?: string; + }[] | null; + /** @description ISO 8601 with offset. Apple Wallet surfaces the pass around this time. Apple only. */ + relevantDate?: string | null; + }; + platforms?: ("apple" | "google")[]; + starter_template_id?: string; + require_download_link?: boolean; + redemption_policy?: components["schemas"]["RedemptionPolicyInput"]; + }; + RedemptionPolicyInput: { + /** @enum {string} */ + mode: "single_use" | "reusable" | "count"; + /** @description Required when mode is count. */ + max_uses?: number; + }; + UpdateTemplateBody: { + name?: string; + design?: { + description?: string | null; + logoText?: string | null; + foregroundColor: string; + backgroundColor: string; + labelColor: string; + /** @description Storage key from an image upload. Leave null to keep none. */ + logoImageKey?: string | null; + /** @description Storage key from an image upload. Leave null to keep none. */ + iconImageKey?: string | null; + /** @description Storage key from an image upload. Leave null to keep none. */ + stripImageKey?: string | null; + /** @description Storage key from an image upload. Leave null to keep none. */ + thumbnailImageKey?: string | null; + headerFields?: { + /** @description Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design. */ + key: string; + label?: string | null; + defaultValue?: string | null; + /** @enum {string|null} */ + textAlignment?: "left" | "center" | "right" | "natural" | null; + required?: unknown; + /** @description Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder. */ + changeMessage?: string | null; + }[] | null; + primaryFields?: { + /** @description Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design. */ + key: string; + label?: string | null; + defaultValue?: string | null; + /** @enum {string|null} */ + textAlignment?: "left" | "center" | "right" | "natural" | null; + required?: unknown; + /** @description Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder. */ + changeMessage?: string | null; + }[] | null; + secondaryFields?: { + /** @description Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design. */ + key: string; + label?: string | null; + defaultValue?: string | null; + /** @enum {string|null} */ + textAlignment?: "left" | "center" | "right" | "natural" | null; + required?: unknown; + /** @description Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder. */ + changeMessage?: string | null; + }[] | null; + auxiliaryFields?: { + /** @description Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design. */ + key: string; + label?: string | null; + defaultValue?: string | null; + /** @enum {string|null} */ + textAlignment?: "left" | "center" | "right" | "natural" | null; + required?: unknown; + /** @description Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder. */ + changeMessage?: string | null; + }[] | null; + backFields?: { + /** @description Stable identifier for this field — the key you pass in field_values when issuing or updating a pass. Letters, numbers and underscores; unique across the design. */ + key: string; + label?: string | null; + defaultValue?: string | null; + /** @enum {string|null} */ + textAlignment?: "left" | "center" | "right" | "natural" | null; + required?: unknown; + /** @description Apple Wallet lock-screen notification for this field, e.g. 'Your seat is now %@'. Must contain %@, which is replaced by the new value. This is the only way an update becomes visible to the holder. */ + changeMessage?: string | null; + }[] | null; + /** @enum {string|null} */ + barcodeFormat?: "PKBarcodeFormatQR" | "PKBarcodeFormatPDF417" | "PKBarcodeFormatAztec" | "PKBarcodeFormatCode128" | null; + /** @description What the barcode encodes. Use '{{serialNumber}}' — scanning looks passes up by serial number, and it is unique per pass. '{{fieldValues.}}' inserts a field value. No other placeholders exist. */ + barcodeMessageTemplate?: string | null; + /** @description The issuer shown on the pass and in its notifications (Apple organizationName; Google issuer/program name unless logoText is set). Defaults to your organization's name. */ + issuerName?: string | null; + /** @description Up to 10 places the pass is relevant near. Apple Wallet suggests the pass on the lock screen there; Google Wallet stores them on the pass. */ + locations?: { + latitude: number; + longitude: number; + altitude?: number; + /** @description Lock-screen text Apple Wallet shows near this location. */ + relevantText?: string; + }[] | null; + /** @description ISO 8601 with offset. Apple Wallet surfaces the pass around this time. Apple only. */ + relevantDate?: string | null; + }; + archived?: boolean; + platforms?: ("apple" | "google")[]; + certificate_set_id?: string | null; + google_issuer_id?: string | null; + require_download_link?: boolean; + redemption_policy?: components["schemas"]["RedemptionPolicyInput"]; + }; + CreateWebhookBody: { + /** + * @description The HTTPS URL events are POSTed to. + * @example https://example.com/hooks/passmint + */ + url: string; + /** + * @description Required, non-empty. Event types to deliver, or `*` for all. + * @example [ + * "pass.redeemed" + * ] + */ + events: string[]; + description?: string | null; + /** @description Defaults to true. */ + enabled?: boolean; + }; + UpdateWebhookBody: { + url?: string; + /** + * @description Event types to deliver (for example `pass.redeemed`), or `*` for all of them. + * @example [ + * "pass.redeemed" + * ] + */ + events?: string[]; + description?: string | null; + enabled?: boolean; + }; + BackfillBody: { + /** + * @description ISO 8601 start of the window. Required. + * @example 2026-10-01T00:00:00Z + */ + since: string; + /** @description ISO 8601 end of the window. Defaults to now. The window is capped in length. */ + until?: string; + }; + PassIssuedEvent: { + id: string; + /** @enum {string} */ + object: "event"; + /** @enum {string} */ + type: "pass.issued"; + api_version: string; + created_at: string; + idempotency_key: string; + livemode: boolean; + data: { + object: { + pass: components["schemas"]["EventPassSnapshot"]; + }; + }; + source: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + /** @enum {string|null} */ + unit: "device" | "object" | null; + /** @enum {string} */ + confidence: "exact" | "best_effort" | "unconfirmed"; + }; + previous_attributes: null; + }; + EventPassSnapshot: { + id: string; + short_id: string; + template_id: string; + organization_id: string; + serial_number: string; + holder: { + email_hash: string | null; + name_present: boolean; + }; + voided: boolean; + /** @description The template image variant the pass renders with, or null for the template's own images. */ + image_variant: string | null; + url: string; + download_url: string | null; + google_wallet_url: string | null; + }; + PassAddIntentEvent: { + id: string; + /** @enum {string} */ + object: "event"; + /** @enum {string} */ + type: "pass.add_intent"; + api_version: string; + created_at: string; + idempotency_key: string; + livemode: boolean; + data: { + object: { + pass: components["schemas"]["EventPassSnapshot"]; + }; + }; + source: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + /** @enum {string|null} */ + unit: "device" | "object" | null; + /** @enum {string} */ + confidence: "exact" | "best_effort" | "unconfirmed"; + }; + previous_attributes: null; + }; + PassAddedToWalletEvent: { + id: string; + /** @enum {string} */ + object: "event"; + /** @enum {string} */ + type: "pass.added_to_wallet"; + api_version: string; + created_at: string; + idempotency_key: string; + livemode: boolean; + data: { + object: { + pass: components["schemas"]["EventPassSnapshot"]; + }; + }; + source: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + /** @enum {string|null} */ + unit: "device" | "object" | null; + /** @enum {string} */ + confidence: "exact" | "best_effort" | "unconfirmed"; + }; + previous_attributes: null; + }; + PassUpdatePushedEvent: { + id: string; + /** @enum {string} */ + object: "event"; + /** @enum {string} */ + type: "pass.update_pushed"; + api_version: string; + created_at: string; + idempotency_key: string; + livemode: boolean; + data: { + object: { + pass: components["schemas"]["EventPassSnapshot"]; + }; + }; + source: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + /** @enum {string|null} */ + unit: "device" | "object" | null; + /** @enum {string} */ + confidence: "exact" | "best_effort" | "unconfirmed"; + }; + previous_attributes: null; + }; + PassUpdateDeliveredEvent: { + id: string; + /** @enum {string} */ + object: "event"; + /** @enum {string} */ + type: "pass.update_delivered"; + api_version: string; + created_at: string; + idempotency_key: string; + livemode: boolean; + data: { + object: { + pass: components["schemas"]["EventPassSnapshot"]; + }; + }; + source: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + /** @enum {string|null} */ + unit: "device" | "object" | null; + /** @enum {string} */ + confidence: "exact" | "best_effort" | "unconfirmed"; + }; + previous_attributes: null; + }; + PassUpdateFailedEvent: { + id: string; + /** @enum {string} */ + object: "event"; + /** @enum {string} */ + type: "pass.update_failed"; + api_version: string; + created_at: string; + idempotency_key: string; + livemode: boolean; + data: { + object: { + pass: components["schemas"]["EventPassSnapshot"]; + delivery_failure: components["schemas"]["EventDeliveryFailure"]; + }; + }; + source: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + /** @enum {string|null} */ + unit: "device" | "object" | null; + /** @enum {string} */ + confidence: "exact" | "best_effort" | "unconfirmed"; + }; + previous_attributes: null; + }; + /** @description `data.object.delivery_failure`, present on `pass.update_failed`. */ + EventDeliveryFailure: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + reason: string | null; + failed: number | null; + devices: number | null; + /** @enum {string|null} */ + context: "update" | "void" | null; + }; + PassRemovedEvent: { + id: string; + /** @enum {string} */ + object: "event"; + /** @enum {string} */ + type: "pass.removed"; + api_version: string; + created_at: string; + idempotency_key: string; + livemode: boolean; + data: { + object: { + pass: components["schemas"]["EventPassSnapshot"]; + }; + }; + source: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + /** @enum {string|null} */ + unit: "device" | "object" | null; + /** @enum {string} */ + confidence: "exact" | "best_effort" | "unconfirmed"; + }; + previous_attributes: null; + }; + PassVoidedEvent: { + id: string; + /** @enum {string} */ + object: "event"; + /** @enum {string} */ + type: "pass.voided"; + api_version: string; + created_at: string; + idempotency_key: string; + livemode: boolean; + data: { + object: { + pass: components["schemas"]["EventPassSnapshot"]; + }; + }; + source: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + /** @enum {string|null} */ + unit: "device" | "object" | null; + /** @enum {string} */ + confidence: "exact" | "best_effort" | "unconfirmed"; + }; + previous_attributes: null; + }; + PassRedeemedEvent: { + id: string; + /** @enum {string} */ + object: "event"; + /** @enum {string} */ + type: "pass.redeemed"; + api_version: string; + created_at: string; + idempotency_key: string; + livemode: boolean; + data: { + object: { + pass: components["schemas"]["EventPassSnapshot"]; + redemption: components["schemas"]["EventRedemption"]; + }; + }; + source: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + /** @enum {string|null} */ + unit: "device" | "object" | null; + /** @enum {string} */ + confidence: "exact" | "best_effort" | "unconfirmed"; + }; + previous_attributes: null; + }; + /** @description `data.object.redemption`, present on `pass.redeemed`. */ + EventRedemption: { + id: string | null; + scanner_device_id: string | null; + scans_used: number | null; + policy_mode: string | null; + }; + ScannerPairing: { + /** @enum {string} */ + object: "scanner_pairing"; + token: string; + device: { + id: string; + name: string; + /** @enum {string} */ + mode: "test" | "live"; + template_id: string | null; + }; + }; + Error: { + error: { + /** @enum {string} */ + type: "authentication_error" | "authorization_error" | "invalid_request_error" | "not_found_error" | "rate_limit_error" | "idempotency_error" | "api_error" | "billing_error"; + /** + * @description A stable, machine-readable code when the error has a specific cause. Not exhaustive: treat unknown codes as the generic error for the `type`. Codes include `api_key_expired` (401, the key's expiry has passed), `insufficient_scope` (403, the key's scopes do not allow this route), `invalid_limit` (400, `limit` is not a positive integer within the maximum), `invalid_cursor` (400, `starting_after` is not an id on this list) and `plan_limit_exceeded` (402). + * @example api_key_expired + * @example insufficient_scope + * @example invalid_limit + * @example invalid_cursor + */ + code?: string; + message: string; + param?: string; + }; + }; + ScanCodeRequest: { + code: string; + }; + Account: { + /** @enum {string} */ + object: "account"; + organization_id: string; + organization_name: string; + organization_slug: string | null; + /** @enum {string} */ + mode: "test" | "live"; + plan: { + id: string; + name: string; + }; + /** @description Live-mode usage in the current billing period (a rolling 30 days on plans without one). Pass issuance stops at api_calls.quota; other live writes continue to api_calls.grace_ceiling. */ + usage: { + period_start: string | null; + api_calls: { + used: number; + quota: number | null; + grace_ceiling: number | null; + }; + active_passes: { + used: number; + included: number; + ceiling: number; + }; + }; + /** @description The organization's own signing credentials. Live passes are only signed with these, never with Passmint's shared test credentials. */ + credentials: { + apple_certificates: { + id: string; + name: string; + pass_type_identifier: string | null; + /** @enum {string} */ + status: "pending_certificate" | "active" | "revoked"; + expires_at: string | null; + }[]; + google_issuers: { + id: string; + name: string; + issuer_id: string; + /** @enum {string} */ + status: "pending" | "active" | "revoked"; + }[]; + }; + }; + EventList: { + /** @enum {string} */ + object: "list"; + data: components["schemas"]["Event"][]; + has_more: boolean; + }; + Event: { + id: string; + /** @enum {string} */ + object: "event"; + /** @enum {string} */ + type: "pass.issued" | "pass.add_intent" | "pass.added_to_wallet" | "pass.update_pushed" | "pass.update_delivered" | "pass.update_failed" | "pass.removed" | "pass.voided" | "pass.redeemed"; + api_version: string; + created_at: string; + idempotency_key: string; + livemode: boolean; + data: { + object: { + pass: components["schemas"]["EventPassSnapshot"]; + redemption?: components["schemas"]["EventRedemption"]; + delivery_failure?: components["schemas"]["EventDeliveryFailure"]; + }; + }; + source: { + /** @enum {string|null} */ + platform: "apple" | "google" | null; + /** @enum {string|null} */ + unit: "device" | "object" | null; + /** @enum {string} */ + confidence: "exact" | "best_effort" | "unconfirmed"; + }; + previous_attributes: null; + }; + Funnel: { + /** @enum {string} */ + object: "funnel"; + /** @enum {string} */ + group_by: "total" | "template" | "platform" | "date" | "mode"; + data: components["schemas"]["FunnelSummary"][]; + confidence: components["schemas"]["FunnelConfidence"]; + }; + FunnelSummary: { + key: string; + issued: number; + add_intent: number; + added: number; + active: number; + removed: number; + update_pushed: number; + update_delivered: number; + install_rate: number | null; + removal_rate: number | null; + update_delivery_rate: number | null; + }; + FunnelConfidence: { + removed: string; + update_delivery_rate: string; + active: string; + }; + TemplateList: { + /** @enum {string} */ + object: "list"; + data: components["schemas"]["Template"][]; + has_more: boolean; + }; + Template: { + id: string; + /** @enum {string} */ + object: "template"; + name: string; + type: string; + apple_style: string; + starter_template_id: string | null; + design: components["schemas"]["TemplateDesign"]; + archived: boolean; + platforms: ("apple" | "google")[]; + certificate_set_id: string | null; + google_issuer_id: string | null; + require_download_link: boolean; + redemption_policy: components["schemas"]["RedemptionPolicy"]; + /** @description Present only when the saved design breaks a recommended rule (an Apple field limit, a duplicate field key, an unknown barcode placeholder, an image the pass style cannot show). The template is saved; fix these before they surprise you at issuance. */ + warnings?: string[]; + /** + * @description The template's image variant names that hold at least one image, sorted. A pass selects one with `image_variant`. + * @example [ + * "0", + * "1", + * "2", + * "reward" + * ] + */ + image_variants: string[]; + created_at: string; + updated_at: string; + }; + /** @description The template's design as stored. See CreateTemplateBody.design for the write contract. */ + TemplateDesign: { + description?: string | null; + logoText?: string | { + default: string; + translations?: { + [key: string]: string; + }; + } | null; + foregroundColor?: string; + backgroundColor?: string; + labelColor?: string; + headerFields?: ({ + key: string; + label?: string | { + default: string; + translations?: { + [key: string]: string; + }; + } | null; + defaultValue?: string | number | null; + textAlignment?: string; + changeMessage?: string | null; + } & { + [key: string]: unknown; + })[] | null; + primaryFields?: ({ + key: string; + label?: string | { + default: string; + translations?: { + [key: string]: string; + }; + } | null; + defaultValue?: string | number | null; + textAlignment?: string; + changeMessage?: string | null; + } & { + [key: string]: unknown; + })[] | null; + secondaryFields?: ({ + key: string; + label?: string | { + default: string; + translations?: { + [key: string]: string; + }; + } | null; + defaultValue?: string | number | null; + textAlignment?: string; + changeMessage?: string | null; + } & { + [key: string]: unknown; + })[] | null; + auxiliaryFields?: ({ + key: string; + label?: string | { + default: string; + translations?: { + [key: string]: string; + }; + } | null; + defaultValue?: string | number | null; + textAlignment?: string; + changeMessage?: string | null; + } & { + [key: string]: unknown; + })[] | null; + backFields?: ({ + key: string; + label?: string | { + default: string; + translations?: { + [key: string]: string; + }; + } | null; + defaultValue?: string | number | null; + textAlignment?: string; + changeMessage?: string | null; + } & { + [key: string]: unknown; + })[] | null; + barcodeFormat?: string | null; + barcodeMessageTemplate?: string | null; + issuerName?: string | null; + locations?: { + [key: string]: unknown; + }[] | null; + relevantDate?: string | null; + } & { + [key: string]: unknown; + }; + RedemptionPolicy: { + /** @enum {string} */ + mode: "single_use" | "reusable" | "count"; + maxUses?: number; + }; + DeletedTemplate: { + id: string; + /** @enum {boolean} */ + deleted: true; + }; + TemplateImage: { + /** @enum {string} */ + object: "template_image"; + template_id: string; + slot: string; + variant: string | null; + }; + DeletedTemplateImage: components["schemas"]["TemplateImage"] & { + /** @enum {boolean} */ + deleted: true; + }; + WebhookList: { + /** @enum {string} */ + object: "list"; + data: components["schemas"]["Webhook"][]; + has_more: boolean; + }; + Webhook: { + id: string; + /** @enum {string} */ + object: "webhook"; + url: string; + events: string[]; + description: string | null; + enabled: boolean; + secret?: string; + created_at: string; + }; + DeletedWebhook: { + id: string; + /** @enum {boolean} */ + deleted: true; + }; + WebhookDeliveryList: { + /** @enum {string} */ + object: "list"; + data: components["schemas"]["WebhookDelivery"][]; + has_more: boolean; + }; + WebhookDelivery: { + id: string; + /** @enum {string} */ + object: "webhook_delivery"; + webhook_id: string; + event_type: string; + status: string; + attempts: number; + last_attempt_at: string | null; + next_attempt_at: string | null; + response_status: number | null; + response_body: string | null; + created_at: string; + }; + WebhookBackfill: { + /** @enum {string} */ + object: "backfill"; + enqueued: number; + capped: boolean; + window: { + since: string; + until: string; + }; + }; + PassList: { + /** @enum {string} */ + object: "list"; + data: components["schemas"]["Pass"][]; + has_more: boolean; + }; + Pass: { + id: string; + /** @enum {string} */ + object: "pass"; + short_id: string; + template_id: string; + certificate_set_id: string | null; + serial_number: string; + mode: string; + holder_email: string | null; + holder_name: string | null; + /** @description The pass's field values by field key. Normally strings; any JSON value sent is stored and returned as-is. */ + field_values: { + [key: string]: unknown; + }; + voided: boolean; + voided_at: string | null; + redeemed_count: number; + first_redeemed_at: string | null; + /** @description Free-form key/value data you attached to the pass, or null. */ + metadata: { + [key: string]: unknown; + } | null; + /** @description The template image variant this pass renders with, or null for the template's own images. */ + image_variant: string | null; + /** + * @description Every image slot the pass renders, keyed by Apple slot name, with the layer it comes from: `pass` (this pass's own image, `PUT /v1/passes/{id}/images/{slot}`), `variant` (its `image_variant`) or `template`. Carries no URLs. + * @example { + * "icon": { + * "source": "template" + * }, + * "logo": { + * "source": "template" + * }, + * "thumbnail": { + * "source": "pass" + * } + * } + */ + images: { + [key: string]: { + /** @enum {string} */ + source: "pass" | "variant" | "template"; + }; + }; + created_via_api: boolean; + /** @description The wallets this pass was issued for. */ + platforms: ("apple" | "google")[]; + /** @description Per-wallet delivery outcome. A wallet is absent when the pass was not issued for it. */ + platform_status: { + /** @enum {string} */ + apple?: "delivered" | "skipped_no_credential" | "failed"; + /** @enum {string} */ + google?: "delivered" | "skipped_no_credential" | "failed"; + } | null; + url: string; + download_url: string | null; + google_wallet_url: string | null; + created_at: string; + }; + PassCreateResponse: { + id: string; + /** @enum {string} */ + object: "pass"; + short_id: string; + template_id: string; + certificate_set_id: string | null; + serial_number: string; + mode: string; + holder_email: string | null; + holder_name: string | null; + /** @description The pass's field values by field key. Normally strings; any JSON value sent is stored and returned as-is. */ + field_values: { + [key: string]: unknown; + }; + voided: boolean; + voided_at: string | null; + redeemed_count: number; + first_redeemed_at: string | null; + /** @description Free-form key/value data you attached to the pass, or null. */ + metadata: { + [key: string]: unknown; + } | null; + /** @description The template image variant this pass renders with, or null for the template's own images. */ + image_variant: string | null; + /** + * @description Every image slot the pass renders, keyed by Apple slot name, with the layer it comes from: `pass` (this pass's own image, `PUT /v1/passes/{id}/images/{slot}`), `variant` (its `image_variant`) or `template`. Carries no URLs. + * @example { + * "icon": { + * "source": "template" + * }, + * "logo": { + * "source": "template" + * }, + * "thumbnail": { + * "source": "pass" + * } + * } + */ + images: { + [key: string]: { + /** @enum {string} */ + source: "pass" | "variant" | "template"; + }; + }; + created_via_api: boolean; + /** @description The wallets this pass was issued for. */ + platforms: ("apple" | "google")[]; + /** @description Per-wallet delivery outcome. A wallet is absent when the pass was not issued for it. */ + platform_status: { + /** @enum {string} */ + apple?: "delivered" | "skipped_no_credential" | "failed"; + /** @enum {string} */ + google?: "delivered" | "skipped_no_credential" | "failed"; + } | null; + url: string; + download_url: string | null; + google_wallet_url: string | null; + created_at: string; + warnings: string[]; + }; + PassUpdateResponse: { + id: string; + /** @enum {string} */ + object: "pass"; + short_id: string; + template_id: string; + certificate_set_id: string | null; + serial_number: string; + mode: string; + holder_email: string | null; + holder_name: string | null; + /** @description The pass's field values by field key. Normally strings; any JSON value sent is stored and returned as-is. */ + field_values: { + [key: string]: unknown; + }; + voided: boolean; + voided_at: string | null; + redeemed_count: number; + first_redeemed_at: string | null; + /** @description Free-form key/value data you attached to the pass, or null. */ + metadata: { + [key: string]: unknown; + } | null; + /** @description The template image variant this pass renders with, or null for the template's own images. */ + image_variant: string | null; + /** + * @description Every image slot the pass renders, keyed by Apple slot name, with the layer it comes from: `pass` (this pass's own image, `PUT /v1/passes/{id}/images/{slot}`), `variant` (its `image_variant`) or `template`. Carries no URLs. + * @example { + * "icon": { + * "source": "template" + * }, + * "logo": { + * "source": "template" + * }, + * "thumbnail": { + * "source": "pass" + * } + * } + */ + images: { + [key: string]: { + /** @enum {string} */ + source: "pass" | "variant" | "template"; + }; + }; + created_via_api: boolean; + /** @description The wallets this pass was issued for. */ + platforms: ("apple" | "google")[]; + /** @description Per-wallet delivery outcome. A wallet is absent when the pass was not issued for it. */ + platform_status: { + /** @enum {string} */ + apple?: "delivered" | "skipped_no_credential" | "failed"; + /** @enum {string} */ + google?: "delivered" | "skipped_no_credential" | "failed"; + } | null; + url: string; + download_url: string | null; + google_wallet_url: string | null; + created_at: string; + delivery: components["schemas"]["PassDelivery"]; + warnings: string[]; + }; + PassDelivery: { + /** @description Null when the pass was not delivered to Apple Wallet. `failed` excludes dead tokens (`unregistered`), which mean the holder deleted the pass. */ + apple: { + devices: number; + pushed: number; + failed: number; + unregistered: number; + } | null; + /** @description Null when the pass has no Google Wallet object. `not_saved`: no holder has saved it yet. `skipped`: Google delivery was skipped at issue. */ + google: { + /** @enum {string} */ + status: "updated" | "not_saved" | "failed" | "skipped"; + error?: string; + } | null; + }; + PassVoidResponse: { + id: string; + /** @enum {string} */ + object: "pass"; + short_id: string; + template_id: string; + certificate_set_id: string | null; + serial_number: string; + mode: string; + holder_email: string | null; + holder_name: string | null; + /** @description The pass's field values by field key. Normally strings; any JSON value sent is stored and returned as-is. */ + field_values: { + [key: string]: unknown; + }; + voided: boolean; + voided_at: string | null; + redeemed_count: number; + first_redeemed_at: string | null; + /** @description Free-form key/value data you attached to the pass, or null. */ + metadata: { + [key: string]: unknown; + } | null; + /** @description The template image variant this pass renders with, or null for the template's own images. */ + image_variant: string | null; + /** + * @description Every image slot the pass renders, keyed by Apple slot name, with the layer it comes from: `pass` (this pass's own image, `PUT /v1/passes/{id}/images/{slot}`), `variant` (its `image_variant`) or `template`. Carries no URLs. + * @example { + * "icon": { + * "source": "template" + * }, + * "logo": { + * "source": "template" + * }, + * "thumbnail": { + * "source": "pass" + * } + * } + */ + images: { + [key: string]: { + /** @enum {string} */ + source: "pass" | "variant" | "template"; + }; + }; + created_via_api: boolean; + /** @description The wallets this pass was issued for. */ + platforms: ("apple" | "google")[]; + /** @description Per-wallet delivery outcome. A wallet is absent when the pass was not issued for it. */ + platform_status: { + /** @enum {string} */ + apple?: "delivered" | "skipped_no_credential" | "failed"; + /** @enum {string} */ + google?: "delivered" | "skipped_no_credential" | "failed"; + } | null; + url: string; + download_url: string | null; + google_wallet_url: string | null; + created_at: string; + warnings: string[]; + }; + PassEventList: { + /** @enum {string} */ + object: "list"; + data: components["schemas"]["PassEvent"][]; + has_more: boolean; + }; + PassEvent: { + id: string; + /** @enum {string} */ + object: "pass_event"; + pass_id: string; + type: string; + /** @description Event-specific detail, such as the platform or a redemption's scanner. Shape varies by `type`; null when the event carries none. */ + metadata: { + [key: string]: unknown; + } | null; + created_at: string; + }; + RedemptionList: { + /** @enum {string} */ + object: "list"; + data: components["schemas"]["Redemption"][]; + has_more: boolean; + }; + Redemption: { + id: string; + /** @enum {string} */ + object: "redemption"; + /** @enum {string} */ + decision: "accepted" | "rejected"; + /** @enum {string|null} */ + reason: "already_redeemed" | "count_exhausted" | "voided" | "not_found" | "out_of_scope" | "wrong_mode" | null; + scanner_name: string | null; + scanned_at: string; + }; + PassDownloadLink: { + /** @enum {string} */ + object: "pass.download_link"; + pass_id: string; + url: string; + expires_in: number; + expires_at: string; + created_at: string; + }; + PassImageResponse: { + id: string; + /** @enum {string} */ + object: "pass"; + short_id: string; + template_id: string; + certificate_set_id: string | null; + serial_number: string; + mode: string; + holder_email: string | null; + holder_name: string | null; + /** @description The pass's field values by field key. Normally strings; any JSON value sent is stored and returned as-is. */ + field_values: { + [key: string]: unknown; + }; + voided: boolean; + voided_at: string | null; + redeemed_count: number; + first_redeemed_at: string | null; + /** @description Free-form key/value data you attached to the pass, or null. */ + metadata: { + [key: string]: unknown; + } | null; + /** @description The template image variant this pass renders with, or null for the template's own images. */ + image_variant: string | null; + /** + * @description Every image slot the pass renders, keyed by Apple slot name, with the layer it comes from: `pass` (this pass's own image, `PUT /v1/passes/{id}/images/{slot}`), `variant` (its `image_variant`) or `template`. Carries no URLs. + * @example { + * "icon": { + * "source": "template" + * }, + * "logo": { + * "source": "template" + * }, + * "thumbnail": { + * "source": "pass" + * } + * } + */ + images: { + [key: string]: { + /** @enum {string} */ + source: "pass" | "variant" | "template"; + }; + }; + created_via_api: boolean; + /** @description The wallets this pass was issued for. */ + platforms: ("apple" | "google")[]; + /** @description Per-wallet delivery outcome. A wallet is absent when the pass was not issued for it. */ + platform_status: { + /** @enum {string} */ + apple?: "delivered" | "skipped_no_credential" | "failed"; + /** @enum {string} */ + google?: "delivered" | "skipped_no_credential" | "failed"; + } | null; + url: string; + download_url: string | null; + google_wallet_url: string | null; + created_at: string; + warnings: string[]; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record; diff --git a/test/generated.test.ts b/test/generated.test.ts new file mode 100644 index 0000000..2746c1d --- /dev/null +++ b/test/generated.test.ts @@ -0,0 +1,11 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { PASSMINT_EVENT_TYPES as GENERATED } from '../src/generated/event-types' + +describe('generated code', () => { + it('lists exactly the event types the committed spec publishes', () => { + const spec = JSON.parse(readFileSync(new URL('../openapi.json', import.meta.url), 'utf8')) + + expect([...GENERATED].sort()).toEqual(Object.keys(spec.webhooks).sort()) + }) +}) From d540fd50a4e1b3d543f11b703269d0233c096afd Mon Sep 17 00:00:00 2001 From: Alex Pate Date: Thu, 8 Oct 2026 15:58:21 +0200 Subject: [PATCH 2/8] Derive public types from the generated spec --- src/index.ts | 5 + src/types.ts | 227 +++++-------------- test/compat-0.4.test-d.ts | 109 +++++++++ test/exports.test.ts | 35 +++ test/fixtures/types-0.4.0.ts | 420 +++++++++++++++++++++++++++++++++++ test/index.test.ts | 2 +- test/webhooks.test.ts | 4 +- 7 files changed, 630 insertions(+), 172 deletions(-) create mode 100644 test/compat-0.4.test-d.ts create mode 100644 test/exports.test.ts create mode 100644 test/fixtures/types-0.4.0.ts diff --git a/src/index.ts b/src/index.ts index 9c7fe42..4b01c40 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ export type { CreatePassParams, CreateTemplateParams, CreateWebhookParams, + DownloadLink, EventPass, EventSource, FunnelGroupBy, @@ -34,7 +35,9 @@ export type { ListResponse, MinimizedHolder, Pass, + PassDelivery, PassEvent, + PassEventType, PassImages, PassImageSlot, PassImageSource, @@ -43,6 +46,8 @@ export type { PassmintMode, PassPlatformStatus, PlatformDeliveryStatus, + Redemption, + RedemptionPolicy, RequestOptions, Template, TemplateDesign, diff --git a/src/types.ts b/src/types.ts index d53b5b9..fce22cf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,11 @@ +import type { PassmintEventType } from './generated/event-types' +import type { components } from './generated/openapi' + +export { PASSMINT_EVENT_TYPES } from './generated/event-types' +export type { PassmintEventType } from './generated/event-types' + +type Schemas = components['schemas'] + export type PassmintMode = 'test' | 'live' export type WalletPlatform = 'apple' | 'google' @@ -55,25 +63,14 @@ export interface TemplateDesign { barcodeMessageTemplate: string } -export interface Template { - id: string - object: 'template' - name: string +/** + * `type`, `apple_style` and `design` keep the hand-written, narrower types + * (the API documents them as plain strings / a looser design object). + */ +export type Template = Omit & { type: TemplateType apple_style: AppleStyle - starter_template_id: string | null design: TemplateDesign - archived: boolean - platforms: WalletPlatform[] - certificate_set_id: string | null - google_issuer_id: string | null - /** - * Names of the image variants uploaded to this template (see - * `templates.uploadImage`). Passes opt into one via `image_variant`. - */ - image_variants: string[] - created_at: string - updated_at: string } /** Every template image slot that accepts a base (non-variant) image. */ @@ -83,12 +80,8 @@ export type TemplateImageSlot = 'icon' | 'logo' | 'strip' | 'thumbnail' | 'backg export type VariantImageSlot = 'strip' | 'thumbnail' | 'background' /** Returned by `templates.uploadImage` (and, with `deleted: true`, `templates.deleteImage`). */ -export interface TemplateImage { - object: 'template_image' - template_id: string +export type TemplateImage = Omit & { slot: TemplateImageSlot - /** The variant name, or `null` for the slot's base image. */ - variant: string | null } export interface TemplateImageOptions { @@ -111,67 +104,37 @@ export type PassImageSource = 'pass' | 'variant' | 'template' /** The resolved image for each slot the pass renders with. Slots without an image are absent. */ export type PassImages = Partial> -export interface Pass { - id: string - object: 'pass' - short_id: string - template_id: string - certificate_set_id: string | null - serial_number: string +export type Pass = Omit & { mode: PassmintMode - holder_email: string | null - holder_name: string | null + /** Field values by field key. Typed as strings, as in 0.4.0; the API stores whatever JSON you send. */ field_values: Record - voided: boolean - voided_at: string | null - metadata: Record | null - created_via_api: boolean - platforms: WalletPlatform[] + /** Per-platform delivery outcome. */ platform_status: PassPlatformStatus | null - /** Passmint-hosted pass page; platform-detects and is always present. Use as your fallback. */ - url: string - /** - * Direct download of the Apple `.pkpass`. Populated when Apple issuance - * succeeded; `null` when Apple wasn't delivered for this pass. Fall back to - * `url`. - */ - download_url: string | null - /** - * Google Wallet "Save" link. Populated when Google issuance succeeded; `null` - * when Google wasn't delivered (e.g. no Google issuer configured). Fall back - * to `url`. - */ - google_wallet_url: string | null - /** - * The template image variant this pass renders with, or `null` for the - * template's base images. - */ - image_variant: string | null /** Where each of the pass's images resolves from (pass → variant → template). */ images: PassImages - created_at: string } -export interface PassEvent { - id: string - object: 'pass_event' - pass_id: string +/** The internal pass-event vocabulary. Intentionally distinct from the canonical `pass.*` event types. */ +export type PassEventType = + | 'created' + | 'url_viewed' + | 'downloaded' + | 'installed' + | 'updated' + | 'update_delivered' + | 'update_failed' + | 'removed' + | 'voided' + | 'google_save_clicked' + | 'redeemed' + | 'download_link_created' + +export type PassEvent = Omit & { /** * Internal event vocabulary — intentionally distinct from the canonical * `pass.*` types that GET /v1/events and webhooks emit. */ - type: - | 'created' - | 'url_viewed' - | 'downloaded' - | 'installed' - | 'updated' - | 'update_delivered' - | 'removed' - | 'voided' - | 'google_save_clicked' - metadata: Record | null - created_at: string + type: PassEventType } export interface ListResponse { @@ -180,70 +143,23 @@ export interface ListResponse { has_more: boolean } -/** - * The Passmint Spec v1 — canonical lifecycle event types delivered to - * webhooks and returned by GET /v1/events. Platform detail lives in `source`. - */ -export const PASSMINT_EVENT_TYPES = [ - 'pass.issued', - 'pass.add_intent', - 'pass.added_to_wallet', - 'pass.update_pushed', - 'pass.update_delivered', - 'pass.removed', - 'pass.voided', -] as const - -export type PassmintEventType = (typeof PASSMINT_EVENT_TYPES)[number] - -export interface EventSource { - platform: 'apple' | 'google' | null - unit: 'device' | 'object' | null - confidence: 'exact' | 'best_effort' | 'unconfirmed' -} +export type EventSource = Schemas['Event']['source'] /** * PII-minimized holder block carried on event payloads. The raw holder * email/name are only retrievable via GET /v1/passes/:id. */ -export interface MinimizedHolder { - email_hash: string | null - name_present: boolean -} +export type MinimizedHolder = Schemas['EventPassSnapshot']['holder'] /** The pass snapshot embedded in a canonical event's `data.object`. */ -export interface EventPass { - id: string - short_id: string - template_id: string - organization_id: string - serial_number: string - holder: MinimizedHolder - voided: boolean - /** Passmint-hosted pass page; platform-detects and is always present. */ - url: string - /** Direct Apple `.pkpass` download; `null` when Apple wasn't delivered. Falls back to `url`. */ - download_url: string | null - /** Google Wallet "Save" link; `null` when Google wasn't delivered. Falls back to `url`. */ - google_wallet_url: string | null - /** The template image variant the pass renders with, or `null` for the base images. */ - image_variant: string | null -} +export type EventPass = Schemas['EventPassSnapshot'] /** * Canonical event envelope — the body of every webhook delivery and each * item returned by GET /v1/events. */ -export interface PassmintEvent { - id: string - object: 'event' - type: PassmintEventType - api_version: string - created_at: string - idempotency_key: string - livemode: boolean - data: { object: { pass: EventPass } } - source: EventSource +export type PassmintEvent = Omit & { + /** Always `null` today; typed as in 0.4.0 so existing fixtures keep compiling. */ previous_attributes: Record | null } @@ -311,32 +227,15 @@ export interface RequestOptions { /** A canonical event type, or "*" to subscribe to everything. */ export type WebhookEventSubscription = PassmintEventType | '*' -export interface Webhook { - id: string - object: 'webhook' - url: string +export type Webhook = Omit & { events: WebhookEventSubscription[] - description: string | null - enabled: boolean - /** Only returned on create. Store it — it is not retrievable via the API afterwards. */ - secret?: string - created_at: string } export type WebhookDeliveryStatus = 'pending' | 'in_progress' | 'delivered' | 'failed' | 'dead' -export interface WebhookDelivery { - id: string - object: 'webhook_delivery' - webhook_id: string +export type WebhookDelivery = Omit & { event_type: PassmintEventType status: WebhookDeliveryStatus - attempts: number - last_attempt_at: string | null - next_attempt_at: string | null - response_status: number | null - response_body: string | null - created_at: string } export interface CreateWebhookParams { @@ -359,12 +258,7 @@ export interface BackfillParams { until?: string } -export interface BackfillResult { - object: 'backfill' - enqueued: number - capped: boolean - window: { since: string; until: string } -} +export type BackfillResult = Schemas['WebhookBackfill'] export interface ListEventsParams { type?: PassmintEventType @@ -380,24 +274,9 @@ export interface ListEventsParams { export type FunnelGroupBy = 'total' | 'template' | 'platform' | 'date' | 'mode' -export interface FunnelSummary { - key: string - issued: number - add_intent: number - added: number - active: number - removed: number - update_pushed: number - update_delivered: number - install_rate: number | null - removal_rate: number | null - update_delivery_rate: number | null -} +export type FunnelSummary = Schemas['FunnelSummary'] -export interface FunnelResponse { - object: 'funnel' - group_by: FunnelGroupBy - data: FunnelSummary[] +export type FunnelResponse = Omit & { /** Confidence caveats per metric (e.g. removals under-report). */ confidence: Record } @@ -411,10 +290,18 @@ export interface FunnelParams { groupBy?: FunnelGroupBy } -export interface Account { - object: 'account' - organization_id: string - organization_name: string +export type Account = Omit & { organization_slug: string - mode: PassmintMode } + +/** Delivery counts for a pass update, per wallet. */ +export type PassDelivery = Schemas['PassDelivery'] + +/** Returned by `passes.createDownloadLink`. */ +export type DownloadLink = Schemas['PassDownloadLink'] + +/** One redemption (scan) attempt against a pass. */ +export type Redemption = Schemas['Redemption'] + +/** A template's redemption rule. */ +export type RedemptionPolicy = Schemas['RedemptionPolicy'] diff --git a/test/compat-0.4.test-d.ts b/test/compat-0.4.test-d.ts new file mode 100644 index 0000000..866cb1c --- /dev/null +++ b/test/compat-0.4.test-d.ts @@ -0,0 +1,109 @@ +import { describe, expectTypeOf, it } from 'vitest' +import { PASSMINT_EVENT_TYPES } from '../src/index' +import type * as Now from '../src/index' +import type * as V04 from './fixtures/types-0.4.0' + +// Every type exported from the package entry at 0.4.0 must still exist. +// Response types: the new type is readable as the 0.4.0 type (new -> old). +// Params and unions: anything valid in 0.4.0 is still valid (old -> new). + +describe('0.4.0 -> now: responses stay readable as the 0.4.0 shape', () => { + it('Account', () => expectTypeOf().toMatchTypeOf()) + it('EventPass', () => expectTypeOf().toMatchTypeOf()) + it('EventSource', () => expectTypeOf().toMatchTypeOf()) + it('FunnelResponse', () => expectTypeOf().toMatchTypeOf()) + it('FunnelSummary', () => expectTypeOf().toMatchTypeOf()) + it('MinimizedHolder', () => + expectTypeOf().toMatchTypeOf()) + it('Pass', () => expectTypeOf().toMatchTypeOf()) + it('Template', () => expectTypeOf().toMatchTypeOf()) + it('TemplateImage', () => expectTypeOf().toMatchTypeOf()) + it('BackfillResult', () => expectTypeOf().toMatchTypeOf()) + + it('Webhook (events now include the new event types)', () => { + expectTypeOf().toMatchTypeOf>() + expectTypeOf().toMatchTypeOf() + }) + + it('WebhookDelivery (event_type now includes the new event types)', () => { + expectTypeOf().toMatchTypeOf>() + expectTypeOf().toMatchTypeOf() + }) + + it('PassEvent (type widened with the real event vocabulary)', () => { + expectTypeOf().toMatchTypeOf>() + expectTypeOf().toMatchTypeOf() + }) + + it('PassmintEvent (type is now the union of all event types)', () => { + expectTypeOf().toMatchTypeOf>() + expectTypeOf().toMatchTypeOf() + expectTypeOf().toMatchTypeOf() + }) + + it('ListResponse', () => { + expectTypeOf>().toEqualTypeOf>() + }) +}) + +describe('0.4.0 -> now: params people already build still type-check', () => { + it('BackfillParams', () => expectTypeOf().toMatchTypeOf()) + it('CreatePassParams', () => + expectTypeOf().toMatchTypeOf()) + it('CreateTemplateParams', () => + expectTypeOf().toMatchTypeOf()) + it('CreateWebhookParams', () => + expectTypeOf().toMatchTypeOf()) + it('FunnelParams', () => expectTypeOf().toMatchTypeOf()) + it('ListEventsParams', () => + expectTypeOf().toMatchTypeOf()) + it('ListPassesParams', () => + expectTypeOf().toMatchTypeOf()) + it('UpdatePassParams', () => + expectTypeOf().toMatchTypeOf()) + it('UpdateTemplateParams', () => + expectTypeOf().toMatchTypeOf()) + it('UpdateWebhookParams', () => + expectTypeOf().toMatchTypeOf()) + it('RequestOptions', () => expectTypeOf().toMatchTypeOf()) + it('TemplateImageOptions', () => + expectTypeOf().toMatchTypeOf()) +}) + +describe('0.4.0 -> now: shared unions are unchanged', () => { + it('AppleStyle', () => expectTypeOf().toEqualTypeOf()) + it('BarcodeFormat', () => expectTypeOf().toEqualTypeOf()) + it('FunnelGroupBy', () => expectTypeOf().toEqualTypeOf()) + it('PassImageSlot', () => expectTypeOf().toEqualTypeOf()) + it('PassImageSource', () => + expectTypeOf().toEqualTypeOf()) + it('PassImages', () => expectTypeOf().toEqualTypeOf()) + it('PassPlatformStatus', () => + expectTypeOf().toEqualTypeOf()) + it('PassmintMode', () => expectTypeOf().toEqualTypeOf()) + it('PlatformDeliveryStatus', () => + expectTypeOf().toEqualTypeOf()) + it('TemplateDesign', () => expectTypeOf().toEqualTypeOf()) + it('TemplateField', () => expectTypeOf().toEqualTypeOf()) + it('TemplateImageSlot', () => + expectTypeOf().toEqualTypeOf()) + it('TemplateType', () => expectTypeOf().toEqualTypeOf()) + it('VariantImageSlot', () => + expectTypeOf().toEqualTypeOf()) + it('WalletPlatform', () => expectTypeOf().toEqualTypeOf()) + it('WebhookDeliveryStatus', () => + expectTypeOf().toEqualTypeOf()) +}) + +describe('event types', () => { + it('still contain every 0.4.0 type, plus the new ones', () => { + expectTypeOf().toMatchTypeOf() + expectTypeOf<'pass.redeemed'>().toMatchTypeOf() + expectTypeOf<'pass.update_failed'>().toMatchTypeOf() + expectTypeOf().toMatchTypeOf() + expectTypeOf(PASSMINT_EVENT_TYPES).toMatchTypeOf() + expectTypeOf(PASSMINT_EVENT_TYPES[0]).toMatchTypeOf() + }) +}) + +type WebhookEventSub = V04.WebhookEventSubscription diff --git a/test/exports.test.ts b/test/exports.test.ts new file mode 100644 index 0000000..0679396 --- /dev/null +++ b/test/exports.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import * as sdk from '../src/index' + +// Every runtime export of @passmint/node 0.4.0. +const RUNTIME_EXPORTS_0_4_0 = [ + 'Passmint', + 'PassmintError', + 'PassmintAPIError', + 'PassmintAuthError', + 'PassmintRateLimitError', + 'PASSMINT_EVENT_TYPES', + 'detectMode', + 'VERSION', +] + +describe('0.4.0 runtime exports', () => { + it.each(RUNTIME_EXPORTS_0_4_0)('still exports %s', (name) => { + expect(sdk).toHaveProperty(name) + }) + + it('keeps PASSMINT_EVENT_TYPES a runtime array containing every 0.4.0 type', () => { + expect(Array.isArray(sdk.PASSMINT_EVENT_TYPES)).toBe(true) + for (const t of [ + 'pass.issued', + 'pass.add_intent', + 'pass.added_to_wallet', + 'pass.update_pushed', + 'pass.update_delivered', + 'pass.removed', + 'pass.voided', + ]) { + expect(sdk.PASSMINT_EVENT_TYPES).toContain(t) + } + }) +}) diff --git a/test/fixtures/types-0.4.0.ts b/test/fixtures/types-0.4.0.ts new file mode 100644 index 0000000..7bdeac6 --- /dev/null +++ b/test/fixtures/types-0.4.0.ts @@ -0,0 +1,420 @@ +// Frozen copy of src/types.ts at @passmint/node 0.4.0 (293a5b1). Do not edit. +// PASSMINT_EVENT_TYPES is a type-only union here so the fixture has no runtime code. + +export type PassmintMode = 'test' | 'live' + +export type WalletPlatform = 'apple' | 'google' + +export type PlatformDeliveryStatus = 'delivered' | 'skipped_no_credential' | 'failed' + +/** Per-platform delivery outcome, e.g. `{ apple: "delivered", google: "failed" }`. */ +export type PassPlatformStatus = Partial> + +export type TemplateType = 'event' | 'membership' | 'coupon' | 'loyalty' | 'generic' + +export type AppleStyle = 'eventTicket' | 'generic' | 'storeCard' | 'coupon' | 'boardingPass' + +export type BarcodeFormat = + | 'PKBarcodeFormatQR' + | 'PKBarcodeFormatPDF417' + | 'PKBarcodeFormatAztec' + | 'PKBarcodeFormatCode128' + +export interface TemplateField { + key: string + label: string + defaultValue: string | null + textAlignment: 'left' | 'center' | 'right' | 'natural' + required: boolean + /** + * Apple Wallet change message: a format string shown as a lock-screen + * notification when this field's value changes on a pass update. Must + * contain `%@`, which is replaced with the new value (e.g. + * `"You now have %@ stamps"`). Without it, Apple updates the pass silently. + */ + changeMessage?: string +} + +export interface TemplateDesign { + description: string + logoText: string | null + foregroundColor: string + backgroundColor: string + labelColor: string + logoImageKey: string | null + iconImageKey: string | null + stripImageKey: string | null + thumbnailImageKey: string | null + /** + * Small fields shown top-right of the pass, next to the logo (Apple slot, + * max 3). Optional — omit for templates that don't use them. + */ + headerFields?: TemplateField[] + primaryFields: TemplateField[] + secondaryFields: TemplateField[] + auxiliaryFields: TemplateField[] + backFields: TemplateField[] + barcodeFormat: BarcodeFormat + barcodeMessageTemplate: string +} + +export interface Template { + id: string + object: 'template' + name: string + type: TemplateType + apple_style: AppleStyle + starter_template_id: string | null + design: TemplateDesign + archived: boolean + platforms: WalletPlatform[] + certificate_set_id: string | null + google_issuer_id: string | null + /** + * Names of the image variants uploaded to this template (see + * `templates.uploadImage`). Passes opt into one via `image_variant`. + */ + image_variants: string[] + created_at: string + updated_at: string +} + +/** Every template image slot that accepts a base (non-variant) image. */ +export type TemplateImageSlot = 'icon' | 'logo' | 'strip' | 'thumbnail' | 'background' | 'footer' + +/** The subset of image slots that can also hold named variants. */ +export type VariantImageSlot = 'strip' | 'thumbnail' | 'background' + +/** Returned by `templates.uploadImage` (and, with `deleted: true`, `templates.deleteImage`). */ +export interface TemplateImage { + object: 'template_image' + template_id: string + slot: TemplateImageSlot + /** The variant name, or `null` for the slot's base image. */ + variant: string | null +} + +export interface TemplateImageOptions { + /** + * Named variant to target instead of the base image. Only valid on + * `VariantImageSlot`s. Must match `^[a-z0-9][a-z0-9_-]{0,31}$`. + */ + variant?: string +} + +/** Image slots a single pass can override (see `passes.uploadImage`). Same set as `VariantImageSlot`. */ +export type PassImageSlot = VariantImageSlot + +/** + * Which layer a pass's resolved image comes from: a per-pass override, the + * pass's template image variant, or the template's base image. + */ +export type PassImageSource = 'pass' | 'variant' | 'template' + +/** The resolved image for each slot the pass renders with. Slots without an image are absent. */ +export type PassImages = Partial> + +export interface Pass { + id: string + object: 'pass' + short_id: string + template_id: string + certificate_set_id: string | null + serial_number: string + mode: PassmintMode + holder_email: string | null + holder_name: string | null + field_values: Record + voided: boolean + voided_at: string | null + metadata: Record | null + created_via_api: boolean + platforms: WalletPlatform[] + platform_status: PassPlatformStatus | null + /** Passmint-hosted pass page; platform-detects and is always present. Use as your fallback. */ + url: string + /** + * Direct download of the Apple `.pkpass`. Populated when Apple issuance + * succeeded; `null` when Apple wasn't delivered for this pass. Fall back to + * `url`. + */ + download_url: string | null + /** + * Google Wallet "Save" link. Populated when Google issuance succeeded; `null` + * when Google wasn't delivered (e.g. no Google issuer configured). Fall back + * to `url`. + */ + google_wallet_url: string | null + /** + * The template image variant this pass renders with, or `null` for the + * template's base images. + */ + image_variant: string | null + /** Where each of the pass's images resolves from (pass → variant → template). */ + images: PassImages + created_at: string +} + +export interface PassEvent { + id: string + object: 'pass_event' + pass_id: string + /** + * Internal event vocabulary — intentionally distinct from the canonical + * `pass.*` types that GET /v1/events and webhooks emit. + */ + type: + | 'created' + | 'url_viewed' + | 'downloaded' + | 'installed' + | 'updated' + | 'update_delivered' + | 'removed' + | 'voided' + | 'google_save_clicked' + metadata: Record | null + created_at: string +} + +export interface ListResponse { + object: 'list' + data: T[] + has_more: boolean +} + +/** + * The Passmint Spec v1 — canonical lifecycle event types delivered to + * webhooks and returned by GET /v1/events. Platform detail lives in `source`. + */ +export type PassmintEventType = + | 'pass.issued' + | 'pass.add_intent' + | 'pass.added_to_wallet' + | 'pass.update_pushed' + | 'pass.update_delivered' + | 'pass.removed' + | 'pass.voided' + +export interface EventSource { + platform: 'apple' | 'google' | null + unit: 'device' | 'object' | null + confidence: 'exact' | 'best_effort' | 'unconfirmed' +} + +/** + * PII-minimized holder block carried on event payloads. The raw holder + * email/name are only retrievable via GET /v1/passes/:id. + */ +export interface MinimizedHolder { + email_hash: string | null + name_present: boolean +} + +/** The pass snapshot embedded in a canonical event's `data.object`. */ +export interface EventPass { + id: string + short_id: string + template_id: string + organization_id: string + serial_number: string + holder: MinimizedHolder + voided: boolean + /** Passmint-hosted pass page; platform-detects and is always present. */ + url: string + /** Direct Apple `.pkpass` download; `null` when Apple wasn't delivered. Falls back to `url`. */ + download_url: string | null + /** Google Wallet "Save" link; `null` when Google wasn't delivered. Falls back to `url`. */ + google_wallet_url: string | null + /** The template image variant the pass renders with, or `null` for the base images. */ + image_variant: string | null +} + +/** + * Canonical event envelope — the body of every webhook delivery and each + * item returned by GET /v1/events. + */ +export interface PassmintEvent { + id: string + object: 'event' + type: PassmintEventType + api_version: string + created_at: string + idempotency_key: string + livemode: boolean + data: { object: { pass: EventPass } } + source: EventSource + previous_attributes: Record | null +} + +export interface CreatePassParams { + templateId: string + /** + * Per-pass certificate override. Pass `null` to explicitly use the dev + * cert; omit to inherit the template's default. + */ + certificateSetId?: string | null + holderEmail?: string + holderName?: string + fieldValues?: Record + metadata?: Record + /** Override the template's wallet platforms for this pass. */ + platforms?: WalletPlatform[] + /** + * Render the pass with one of the template's image variants (see + * `Template.image_variants`). Omit to use the base images. + */ + imageVariant?: string +} + +export interface CreateTemplateParams { + name: string + type: TemplateType + appleStyle: AppleStyle + design: TemplateDesign + starterTemplateId?: string + /** Defaults to ["apple"] on the server. */ + platforms?: WalletPlatform[] +} + +export interface UpdateTemplateParams { + name?: string + design?: TemplateDesign + archived?: boolean + platforms?: WalletPlatform[] + /** Pass `null` to clear back to the dev cert. */ + certificateSetId?: string | null + /** Pass `null` to detach the Google issuer. */ + googleIssuerId?: string | null +} + +export interface UpdatePassParams { + fieldValues?: Record + metadata?: Record | null + /** + * Switch the pass to a template image variant. Pass `null` to clear back to + * the base images; omit to leave it unchanged. + */ + imageVariant?: string | null +} + +export interface ListPassesParams { + templateId?: string + holderEmail?: string + limit?: number +} + +export interface RequestOptions { + idempotencyKey?: string +} + +/** A canonical event type, or "*" to subscribe to everything. */ +export type WebhookEventSubscription = PassmintEventType | '*' + +export interface Webhook { + id: string + object: 'webhook' + url: string + events: WebhookEventSubscription[] + description: string | null + enabled: boolean + /** Only returned on create. Store it — it is not retrievable via the API afterwards. */ + secret?: string + created_at: string +} + +export type WebhookDeliveryStatus = 'pending' | 'in_progress' | 'delivered' | 'failed' | 'dead' + +export interface WebhookDelivery { + id: string + object: 'webhook_delivery' + webhook_id: string + event_type: PassmintEventType + status: WebhookDeliveryStatus + attempts: number + last_attempt_at: string | null + next_attempt_at: string | null + response_status: number | null + response_body: string | null + created_at: string +} + +export interface CreateWebhookParams { + url: string + events: WebhookEventSubscription[] + description?: string | null + enabled?: boolean +} + +export interface UpdateWebhookParams { + url?: string + events?: WebhookEventSubscription[] + description?: string | null + enabled?: boolean +} + +export interface BackfillParams { + since: string + /** Defaults to now on the server. */ + until?: string +} + +export interface BackfillResult { + object: 'backfill' + enqueued: number + capped: boolean + window: { since: string; until: string } +} + +export interface ListEventsParams { + type?: PassmintEventType + passId?: string + templateId?: string + since?: string + until?: string + /** Event id to paginate after (keyset cursor). */ + startingAfter?: string + /** Max 200, defaults to 100 on the server. */ + limit?: number +} + +export type FunnelGroupBy = 'total' | 'template' | 'platform' | 'date' | 'mode' + +export interface FunnelSummary { + key: string + issued: number + add_intent: number + added: number + active: number + removed: number + update_pushed: number + update_delivered: number + install_rate: number | null + removal_rate: number | null + update_delivery_rate: number | null +} + +export interface FunnelResponse { + object: 'funnel' + group_by: FunnelGroupBy + data: FunnelSummary[] + /** Confidence caveats per metric (e.g. removals under-report). */ + confidence: Record +} + +export interface FunnelParams { + templateId?: string + platform?: WalletPlatform + since?: string + until?: string + /** Defaults to "total" on the server. */ + groupBy?: FunnelGroupBy +} + +export interface Account { + object: 'account' + organization_id: string + organization_name: string + organization_slug: string + mode: PassmintMode +} diff --git a/test/index.test.ts b/test/index.test.ts index 05a4cdb..72ad938 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -14,6 +14,6 @@ describe('Passmint client', () => { }) it('re-exports the canonical event type list', () => { - expect(PASSMINT_EVENT_TYPES).toHaveLength(7) + expect(PASSMINT_EVENT_TYPES).toHaveLength(9) }) }) diff --git a/test/webhooks.test.ts b/test/webhooks.test.ts index f089cf0..5cce3b3 100644 --- a/test/webhooks.test.ts +++ b/test/webhooks.test.ts @@ -123,15 +123,17 @@ describe('WebhooksResource.constructEvent', () => { }) describe('PASSMINT_EVENT_TYPES', () => { - it('matches the canonical Passmint Spec v1 lifecycle set', () => { + it('matches the generated lifecycle set', () => { expect(PASSMINT_EVENT_TYPES).toEqual([ 'pass.issued', 'pass.add_intent', 'pass.added_to_wallet', 'pass.update_pushed', 'pass.update_delivered', + 'pass.update_failed', 'pass.removed', 'pass.voided', + 'pass.redeemed', ]) expectTypeOf().toEqualTypeOf<(typeof PASSMINT_EVENT_TYPES)[number]>() }) From e7778bf1979d943fd9eaa2aaefe82cabeb709569 Mon Sep 17 00:00:00 2001 From: Alex Pate Date: Thu, 8 Oct 2026 16:02:34 +0200 Subject: [PATCH 3/8] Restore field docs, add update_not_delivered pass event type --- src/types.ts | 48 +++++++++++++++++++++++++++++++++++++-- test/compat-0.4.test-d.ts | 15 ++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index fce22cf..56899c7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,6 +71,11 @@ export type Template = Omit & { mode: PassmintMode - /** Field values by field key. Typed as strings, as in 0.4.0; the API stores whatever JSON you send. */ + /** + * Field values by field key. Normally strings, but any JSON value you sent is + * stored and returned as-is. Typed `Record` for 0.4.0 + * compatibility. + */ field_values: Record + /** Passmint-hosted pass page; platform-detects and is always present. Use as your fallback. */ + url: string + /** + * Direct download of the Apple `.pkpass`. Populated when Apple issuance + * succeeded; `null` when Apple wasn't delivered for this pass. Fall back to + * `url`. + */ + download_url: string | null + /** + * Google Wallet "Save" link. Populated when Google issuance succeeded; `null` + * when Google wasn't delivered (e.g. no Google issuer configured). Fall back + * to `url`. + */ + google_wallet_url: string | null + /** + * The template image variant this pass renders with, or `null` for the + * template's base images. + */ + image_variant: string | null /** Per-platform delivery outcome. */ platform_status: PassPlatformStatus | null /** Where each of the pass's images resolves from (pass → variant → template). */ @@ -121,6 +149,7 @@ export type PassEventType = | 'downloaded' | 'installed' | 'updated' + | 'update_not_delivered' | 'update_delivered' | 'update_failed' | 'removed' @@ -152,7 +181,16 @@ export type EventSource = Schemas['Event']['source'] export type MinimizedHolder = Schemas['EventPassSnapshot']['holder'] /** The pass snapshot embedded in a canonical event's `data.object`. */ -export type EventPass = Schemas['EventPassSnapshot'] +export type EventPass = Schemas['EventPassSnapshot'] & { + /** Passmint-hosted pass page; platform-detects and is always present. */ + url: string + /** Direct Apple `.pkpass` download; `null` when Apple wasn't delivered. Falls back to `url`. */ + download_url: string | null + /** Google Wallet "Save" link; `null` when Google wasn't delivered. Falls back to `url`. */ + google_wallet_url: string | null + /** The template image variant the pass renders with, or `null` for the base images. */ + image_variant: string | null +} /** * Canonical event envelope — the body of every webhook delivery and each @@ -229,6 +267,8 @@ export type WebhookEventSubscription = PassmintEventType | '*' export type Webhook = Omit & { events: WebhookEventSubscription[] + /** Only returned on create. Store it — it is not retrievable via the API afterwards. */ + secret?: string } export type WebhookDeliveryStatus = 'pending' | 'in_progress' | 'delivered' | 'failed' | 'dead' @@ -291,6 +331,10 @@ export interface FunnelParams { } export type Account = Omit & { + /** + * Can be `null` at runtime; typed `string` for 0.4.0 compatibility. Check + * before relying on it. + */ organization_slug: string } diff --git a/test/compat-0.4.test-d.ts b/test/compat-0.4.test-d.ts index 866cb1c..b88ec7d 100644 --- a/test/compat-0.4.test-d.ts +++ b/test/compat-0.4.test-d.ts @@ -95,6 +95,21 @@ describe('0.4.0 -> now: shared unions are unchanged', () => { expectTypeOf().toEqualTypeOf()) }) +describe('remaining 0.4.0 type exports still exist', () => { + it('ApiErrorPayload', () => expectTypeOf().not.toBeAny()) + it('WebhookEvent', () => expectTypeOf().toEqualTypeOf()) + it('PassmintOptions', () => expectTypeOf().not.toBeAny()) +}) + +describe('pass event types', () => { + it('include update_not_delivered and the other platform event types', () => { + expectTypeOf<'update_not_delivered'>().toMatchTypeOf() + expectTypeOf<'update_failed'>().toMatchTypeOf() + expectTypeOf<'redeemed'>().toMatchTypeOf() + expectTypeOf<'download_link_created'>().toMatchTypeOf() + }) +}) + describe('event types', () => { it('still contain every 0.4.0 type, plus the new ones', () => { expectTypeOf().toMatchTypeOf() From aa89f2dde980e711558be7b89dc9586a6e504f40 Mon Sep 17 00:00:00 2001 From: Alex Pate Date: Thu, 8 Oct 2026 16:06:44 +0200 Subject: [PATCH 4/8] Add pagination, download links and redemptions --- README.md | 42 +++++++++-- src/index.ts | 7 ++ src/pagination.ts | 32 ++++++++ src/resources/events.ts | 5 ++ src/resources/passes.ts | 56 ++++++++++++-- src/resources/templates.ts | 21 +++++- src/types.ts | 49 +++++++++++++ src/webhooks.ts | 32 +++++++- test/compat-0.4.test-d.ts | 6 +- test/pagination.test.ts | 44 +++++++++++ test/resources.test.ts | 145 +++++++++++++++++++++++++++++++++++++ test/types.test-d.ts | 52 +++++++++++++ 12 files changed, 473 insertions(+), 18 deletions(-) create mode 100644 src/pagination.ts create mode 100644 test/pagination.test.ts diff --git a/README.md b/README.md index 6f397e0..5029471 100644 --- a/README.md +++ b/README.md @@ -129,12 +129,15 @@ API keys starting with `pmk_test_` run against test mode, `pmk_live_` run agains | --- | --- | | `create(params, options?)` | Issue a new pass against a template. | | `retrieve(id)` | Fetch a pass by id. | -| `list(params?)` | List passes, filter by template or holder email. | -| `update(id, params, options?)` | Update field values or metadata on a pass. | -| `void(id)` | Void a pass. | +| `list(params?)` | List passes, filter by template or holder email. Takes `limit` and `startingAfter`. | +| `listAll(params?)` | Async iterator over every matching pass (see Pagination). | +| `update(id, params, options?)` | Update field values or metadata on a pass. Resolves with the pass plus `delivery` (per-wallet push counts) and `warnings`. | +| `void(id)` | Void a pass. Resolves with the pass plus `warnings`. | | `uploadImage(id, slot, data)` | Give this pass its own `strip`, `thumbnail` or `background` image. Returns the updated pass. | | `deleteImage(id, slot)` | Remove the pass's own image for a slot. Returns the updated pass. | -| `events(id)` | List raw lifecycle events for a pass (internal vocabulary, e.g. `installed`). Use `passmint.events` for the canonical `pass.*` stream. | +| `events(id, params?)` | List raw lifecycle events for a pass (internal vocabulary, e.g. `installed`). Use `passmint.events` for the canonical `pass.*` stream. Takes `limit` and `startingAfter`; `listAllEvents(id)` iterates every page. | +| `redemptions(id, params?)` | The pass's scan log: accepted and rejected redemption attempts. | +| `createDownloadLink(id, params?)` | Create a short-lived download link for the pass (`expiresIn` in seconds). | `create` and `update` accept an optional `idempotencyKey` in `options`. If omitted, the SDK generates one for you so retries are safe by default. @@ -142,6 +145,18 @@ API keys starting with `pmk_test_` run against test mode, `pmk_live_` run agains `create` and `update` accept `imageVariant` to render the pass with one of its template's image variants (see below). On `update`, `null` clears it back to the base images. +#### Download links and redemptions + +```ts +const link = await passmint.passes.createDownloadLink(pass.id, { expiresIn: 3600 }) +console.log(link.url, link.expires_at) + +const { data: scans } = await passmint.passes.redemptions(pass.id, { limit: 20 }) +for (const scan of scans) console.log(scan.decision, scan.reason, scan.scanned_at) +``` + +Templates take a `redemptionPolicy` (`{ mode: 'single_use' | 'reusable' | 'count', maxUses? }`) and `requireDownloadLink` on `create` and `update`. Template responses include `warnings` when the saved design breaks a recommended rule, and template designs accept `issuerName`, `locations` and `relevantDate`. + #### Per-pass images A pass can have its own `strip`, `thumbnail` or `background` image, such as a member photo: @@ -163,7 +178,8 @@ Images are PNG, JPEG or WebP, up to 8 MB. Both methods re-render the pass and pu | --- | --- | | `create(params, options?)` | Create a template. | | `retrieve(id)` | Fetch a template by id. | -| `list()` | List all templates. | +| `list(params?)` | List templates. Archived ones are left out unless `includeArchived: true`. Takes `limit` and `startingAfter`. | +| `listAll(params?)` | Async iterator over every matching template. | | `update(id, params)` | Update name, design, platforms, credentials, or archive state. | | `archive(id)` | Archive a template. | | `uploadImage(id, slot, data, options?)` | Upload a PNG/JPEG (`Uint8Array`, `ArrayBuffer` or `Buffer`) to an image slot, optionally as a named `variant`. | @@ -212,6 +228,18 @@ const next = await passmint.events.list({ startingAfter: data.at(-1)!.id }) Canonical event types: `pass.issued`, `pass.add_intent`, `pass.added_to_wallet`, `pass.update_pushed`, `pass.update_delivered`, `pass.removed`, `pass.voided` (exported as `PASSMINT_EVENT_TYPES`). +### Pagination + +List endpoints return `{ data, has_more }` and take `startingAfter` (the last item's id). To walk every page, use `listAll`, which fetches pages as you iterate and stops when `has_more` is false: + +```ts +for await (const pass of passmint.passes.listAll({ templateId: 'tmpl_123' })) { + console.log(pass.id) +} +``` + +`listAll` exists on `passes`, `templates`, `webhooks` and `events`. For your own list calls, `autoPaginate((startingAfter) => fetchPage(startingAfter))` is exported too. + ### Metrics — `passmint.metrics` ```ts @@ -233,11 +261,11 @@ Manage webhook endpoints: | Method | Description | | --- | --- | | `create(params)` | Create an endpoint. Returns the signing `secret` **once** — store it. | -| `retrieve(id)` / `list()` | Fetch endpoints. | +| `retrieve(id)` / `list(params?)` | Fetch endpoints. `list` takes `limit` and `startingAfter`; `listAll()` iterates every page. | | `update(id, params)` | Change url, subscribed events, description, or enabled state. | | `delete(id)` | Delete an endpoint. | | `backfill(id, { since, until? })` | Re-emit historical events to one endpoint. | -| `deliveries(id)` | Last 100 delivery attempts, newest first. | +| `deliveries(id, params?)` | Delivery attempts, newest first (last 100 by default). Takes `limit` and `startingAfter`; `listAllDeliveries(id)` iterates every page. | | `replayDelivery(id, deliveryId)` | Re-queue a delivery (including dead ones). | ```ts diff --git a/src/index.ts b/src/index.ts index 4b01c40..3beb2b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ export { PassmintRateLimitError, } from './errors' export type { ApiErrorPayload } from './errors' +export { autoPaginate } from './pagination' export { PASSMINT_EVENT_TYPES } from './types' export type { Account, @@ -21,6 +22,7 @@ export type { BackfillResult, BarcodeFormat, CreatePassParams, + CreateDownloadLinkParams, CreateTemplateParams, CreateWebhookParams, DownloadLink, @@ -30,9 +32,14 @@ export type { FunnelParams, FunnelResponse, FunnelSummary, + ListDeliveriesParams, ListEventsParams, + ListPassEventsParams, ListPassesParams, + ListRedemptionsParams, ListResponse, + ListTemplatesParams, + ListWebhooksParams, MinimizedHolder, Pass, PassDelivery, diff --git a/src/pagination.ts b/src/pagination.ts new file mode 100644 index 0000000..c742107 --- /dev/null +++ b/src/pagination.ts @@ -0,0 +1,32 @@ +import type { ListResponse } from './types' + +/** + * Walks a cursor-paginated list: fetches a page, yields its items, and asks for the page + * after the last item's id until `has_more` is false. Stops on an empty page even if the + * server says there is more, so a misbehaving response can't spin forever. + */ +export async function* autoPaginate( + fetchPage: (startingAfter?: string) => Promise>, +): AsyncIterableIterator { + let startingAfter: string | undefined + + for (;;) { + const page = await fetchPage(startingAfter) + + yield* page.data + + const last = page.data.at(-1) + + if (!page.has_more || !last) return + + startingAfter = last.id + } +} + +/** Builds the params for the next page without ever passing `startingAfter: undefined`. */ +export function withCursor

( + params: P, + cursor: string | undefined, +): P & { startingAfter?: string } { + return { ...params, ...(cursor ? { startingAfter: cursor } : {}) } +} diff --git a/src/resources/events.ts b/src/resources/events.ts index 442ec53..32587b3 100644 --- a/src/resources/events.ts +++ b/src/resources/events.ts @@ -1,4 +1,5 @@ import type { PassmintHttpClient } from '../client' +import { autoPaginate, withCursor } from '../pagination' import type { ListEventsParams, ListResponse, PassmintEvent } from '../types' export class EventsResource { @@ -19,4 +20,8 @@ export class EventsResource { }, }) } + /** Iterate every matching event, fetching further pages as needed. */ + listAll(params: ListEventsParams = {}): AsyncIterableIterator { + return autoPaginate((cursor) => this.list(withCursor(params, cursor))) + } } diff --git a/src/resources/passes.ts b/src/resources/passes.ts index 41c0b49..2ba937e 100644 --- a/src/resources/passes.ts +++ b/src/resources/passes.ts @@ -1,12 +1,19 @@ import { type PassmintHttpClient, generateIdempotencyKey } from '../client' import { toBase64 } from '../encoding' +import { autoPaginate, withCursor } from '../pagination' import type { + CreateDownloadLinkParams, CreatePassParams, + DownloadLink, + ListPassEventsParams, ListPassesParams, + ListRedemptionsParams, ListResponse, Pass, + PassDelivery, PassEvent, PassImageSlot, + Redemption, RequestOptions, UpdatePassParams, } from '../types' @@ -53,18 +60,28 @@ export class PassesResource { template_id: params.templateId, holder_email: params.holderEmail, limit: params.limit, + starting_after: params.startingAfter, }, }) } - update(id: string, params: UpdatePassParams, options: RequestOptions = {}): Promise { + /** Iterate every matching pass, fetching further pages as needed. */ + listAll(params: ListPassesParams = {}): AsyncIterableIterator { + return autoPaginate((cursor) => this.list(withCursor(params, cursor))) + } + + update( + id: string, + params: UpdatePassParams, + options: RequestOptions = {}, + ): Promise { const body: Record = {} if (params.fieldValues !== undefined) body.field_values = params.fieldValues if (params.metadata !== undefined) body.metadata = params.metadata // null clears the variant back to the base images, so only omit the key // when truly unset. if (params.imageVariant !== undefined) body.image_variant = params.imageVariant - return this.http.request({ + return this.http.request({ method: 'PATCH', path: `/v1/passes/${encodeURIComponent(id)}`, body, @@ -72,8 +89,8 @@ export class PassesResource { }) } - void(id: string): Promise { - return this.http.request({ + void(id: string): Promise { + return this.http.request({ method: 'DELETE', path: `/v1/passes/${encodeURIComponent(id)}`, }) @@ -106,10 +123,39 @@ export class PassesResource { }) } - events(id: string): Promise> { + events(id: string, params: ListPassEventsParams = {}): Promise> { return this.http.request>({ method: 'GET', path: `/v1/passes/${encodeURIComponent(id)}/events`, + query: { limit: params.limit, starting_after: params.startingAfter }, + }) + } + + /** Iterate every event for this pass, fetching further pages as needed. */ + listAllEvents(id: string, params: ListPassEventsParams = {}): AsyncIterableIterator { + return autoPaginate((cursor) => this.events(id, withCursor(params, cursor))) + } + + /** The pass's scan log: every accepted or rejected redemption attempt, newest first. */ + redemptions(id: string, params: ListRedemptionsParams = {}): Promise> { + return this.http.request>({ + method: 'GET', + path: `/v1/passes/${encodeURIComponent(id)}/redemptions`, + query: { limit: params.limit }, + }) + } + + /** + * Create a short-lived link that downloads this pass. Not an idempotent + * route on the server, so no Idempotency-Key is sent. + */ + createDownloadLink(id: string, params: CreateDownloadLinkParams = {}): Promise { + const body: Record = {} + if (params.expiresIn !== undefined) body.expires_in = params.expiresIn + return this.http.request({ + method: 'POST', + path: `/v1/passes/${encodeURIComponent(id)}/download-links`, + body, }) } } diff --git a/src/resources/templates.ts b/src/resources/templates.ts index 0f4fee9..a834884 100644 --- a/src/resources/templates.ts +++ b/src/resources/templates.ts @@ -1,8 +1,10 @@ import { type PassmintHttpClient, generateIdempotencyKey } from '../client' import { toBase64 } from '../encoding' +import { autoPaginate, withCursor } from '../pagination' import type { CreateTemplateParams, ListResponse, + ListTemplatesParams, RequestOptions, Template, TemplateImage, @@ -24,6 +26,9 @@ export class TemplatesResource { starter_template_id: params.starterTemplateId, } if (params.platforms !== undefined) body.platforms = params.platforms + if (params.redemptionPolicy !== undefined) body.redemption_policy = params.redemptionPolicy + if (params.requireDownloadLink !== undefined) + body.require_download_link = params.requireDownloadLink return this.http.request