{"openapi":"3.0.0","paths":{"/api/v1/customers":{"post":{"operationId":"V1CustomersController_enroll","parameters":[{"name":"idempotency-key","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrollCustomerDto"}}}},"responses":{"400":{"description":"A `customFields` entry was rejected. The message names the offending keys: `RESERVED_CUSTOM_FIELDS: <keys>` — `name`, `email`, `phone`, `status` and `custom` have dedicated request properties (or are derived) and are never settable here. `UNKNOWN_CUSTOM_FIELDS: <keys>` — not one of the card’s declared data-collection questions; a typo is reported rather than silently stored. `INVALID_CUSTOM_FIELD_VALUES: <keys> (expected a string, number or boolean)` — the value is not a scalar. `CUSTOM_FIELD_VALUE_TYPE_MISMATCH: <keys> (value does not match the field’s declared type)` — the value contradicts the card’s declared type for that question: letters into a `number` question, or a value outside a `select` question’s options. Values that used to be accepted and stored as-is now return this 400, matching what the signup form and the CSV import already enforce."},"409":{"description":"Nothing was written. `AMBIGUOUS_CUSTOMER_MATCH` — the `email`/`phone` matched more than one customer on this card. `EXTERNAL_ID_ON_OTHER_CARD` — the `externalCustomerId` is already on a customer of a different card (the body names that `cardId` and `passUserId`); ids are unique across the account, so send a different one per card. Email/phone matching only looks at `cardId`."},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-customers"]}},"/api/v1/customers/lookup":{"get":{"operationId":"V1CustomersController_lookup","parameters":[{"name":"email","required":true,"in":"query","schema":{"type":"string"}},{"name":"phone","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"The customer found by email or phone. `balance` is `{ points }`, `{ stamps }` or `{ points, level }` depending on the card. Amounts are in whole units, the same units the dashboard shows and the write endpoints accept; store credit may have up to 2 decimals (e.g. 25.5)."},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-customers"]}},"/api/v1/customers/{ref}":{"get":{"operationId":"V1CustomersController_getStatus","parameters":[{"name":"ref","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"The customer, their card type and balance. `balance` is `{ points }`, `{ stamps }` or `{ points, level }` depending on the card. Amounts are in whole units, the same units the dashboard shows and the write endpoints accept; store credit may have up to 2 decimals (e.g. 25.5)."},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-customers"]},"patch":{"operationId":"V1CustomersController_update","parameters":[{"name":"ref","required":true,"in":"path","schema":{"type":"string"}},{"name":"idempotency-key","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerDto"}}}},"responses":{"400":{"description":"A `customFields` entry was rejected. The message names the offending keys: `RESERVED_CUSTOM_FIELDS: <keys>` — `name`, `email`, `phone`, `status` and `custom` have dedicated request properties (or are derived) and are never settable here. `UNKNOWN_CUSTOM_FIELDS: <keys>` — not one of the card’s declared data-collection questions; a typo is reported rather than silently stored. `INVALID_CUSTOM_FIELD_VALUES: <keys> (expected a string, number or boolean)` — the value is not a scalar. `CUSTOM_FIELD_VALUE_TYPE_MISMATCH: <keys> (value does not match the field’s declared type)` — the value contradicts the card’s declared type for that question: letters into a `number` question, or a value outside a `select` question’s options. Values that used to be accepted and stored as-is now return this 400, matching what the signup form and the CSV import already enforce."},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-customers"]}},"/api/v1/customers/{ref}/earn":{"post":{"operationId":"V1CustomersController_earn","parameters":[{"name":"ref","required":true,"in":"path","schema":{"type":"string"}},{"name":"idempotency-key","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EarnDto"}}}},"responses":{"201":{"description":"The balance after the award. `balance` is `{ points }`, `{ stamps }` or `{ points, level }` depending on the card. Amounts are in whole units, the same units the dashboard shows and the write endpoints accept; store credit may have up to 2 decimals (e.g. 25.5)."},"400":{"description":"AWARD_WAIT_ACTIVE | AWARD_MAX_PER_AWARD | AWARD_PERIOD_LIMIT — the program earning limit blocked this award; nothing was written"},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-customers"]}},"/api/v1/customers/{ref}/redeem":{"post":{"operationId":"V1CustomersController_redeem","parameters":[{"name":"ref","required":true,"in":"path","schema":{"type":"string"}},{"name":"idempotency-key","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedeemDto"}}}},"responses":{"201":{"description":"The balance after the redemption. `balance` is `{ points }`, `{ stamps }` or `{ points, level }` depending on the card. Amounts are in whole units, the same units the dashboard shows and the write endpoints accept; store credit may have up to 2 decimals (e.g. 25.5)."},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-customers"]}},"/api/v1/customers/{ref}/adjust":{"post":{"operationId":"V1CustomersController_adjust","parameters":[{"name":"ref","required":true,"in":"path","schema":{"type":"string"}},{"name":"idempotency-key","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdjustDto"}}}},"responses":{"201":{"description":"The balance after the correction. `balance` is `{ points }`, `{ stamps }` or `{ points, level }` depending on the card. Amounts are in whole units, the same units the dashboard shows and the write endpoints accept; store credit may have up to 2 decimals (e.g. 25.5)."},"400":{"description":"AWARD_WAIT_ACTIVE | AWARD_MAX_PER_AWARD | AWARD_PERIOD_LIMIT — a balanceCorrection or setPoints that adds value is an award and the program earning limit blocked it; nothing was written. Lowering the balance and store-credit cards are never limited."},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-customers"]}},"/api/v1/cards":{"get":{"operationId":"V1CardsController_list","parameters":[],"responses":{"200":{"description":""},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-cards"]}},"/api/v1/cards/{cardId}":{"patch":{"operationId":"V1CardsController_updateIdentity","parameters":[{"name":"cardId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCardIdentityDto"}}}},"responses":{"200":{"description":""},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-cards"]}},"/api/v1/webhooks":{"post":{"operationId":"V1WebhooksController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookDto"}}}},"responses":{"201":{"description":""},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-webhooks"]},"get":{"operationId":"V1WebhooksController_list","parameters":[],"responses":{"200":{"description":""},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-webhooks"]}},"/api/v1/webhooks/{id}":{"get":{"operationId":"V1WebhooksController_get","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-webhooks"]},"patch":{"operationId":"V1WebhooksController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookDto"}}}},"responses":{"200":{"description":""},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-webhooks"]},"delete":{"operationId":"V1WebhooksController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-webhooks"]}},"/api/v1/webhooks/{id}/deliveries":{"get":{"operationId":"V1WebhooksController_listDeliveries","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-webhooks"]}},"/api/v1/webhooks/deliveries/{deliveryId}/replay":{"post":{"operationId":"V1WebhooksController_replay","parameters":[{"name":"deliveryId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""},"429":{"description":"RATE_LIMITED — see Retry-After"}},"security":[{"ApiKey-auth":[]}],"tags":["v1-webhooks"]}}},"info":{"title":"Passtastic API","description":"","version":"1","contact":{}},"tags":[],"servers":[{"url":"https://api.passtastic.io"}],"components":{"securitySchemes":{"ApiKey-auth":{"type":"apiKey","in":"header","name":"X-Api-Key"}},"schemas":{"CreateIntegrationKeyDto":{"type":"object","properties":{"deviceName":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["customers:read","customers:write","loyalty:write","webhooks:manage","cards:read","cards:write"]}},"mode":{"type":"string","enum":["live","test"],"default":"live"}},"required":["deviceName"]},"CreateWebhookDto":{"type":"object","properties":{"url":{"type":"string","description":"HTTP(S) endpoint that will receive webhook deliveries"},"events":{"description":"Event types to subscribe to (e.g. \"balance.updated\"), or [\"*\"] for all events","type":"array","items":{"type":"string"}}},"required":["url","events"]},"UpdateWebhookDto":{"type":"object","properties":{"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["active","disabled"]}}},"EnrollCustomerDto":{"type":"object","properties":{"externalCustomerId":{"type":"string","description":"Caller-owned customer identifier (CRM contact id, etc.)"},"cardId":{"type":"string","description":"BusinessCard id to enroll the customer on"},"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"initialPoints":{"type":"number","description":"Opening balance for a new customer, in whole units (points, or store credit). Store credit may have up to 2 decimals (e.g. 25.5), or none when the card counts the merchant's own unit."},"customFields":{"type":"object","description":"Answers to the card's data-collection questions, keyed by field key (case-insensitive). Written to the customer record exactly as the get-pass form writes them. Unknown keys are rejected.","example":{"custom_mqp6v9zz":"27919523"}}},"required":["externalCustomerId","cardId"]},"UpdateCustomerDto":{"type":"object","properties":{"externalCustomerId":{"type":"string","description":"Set (or replace) the caller-owned identifier on this customer. Unique per organization — a value already in use returns 409."},"customFields":{"type":"object","description":"Answers to the card's data-collection questions, keyed by field key (case-insensitive). Unknown keys are rejected.","example":{"custom_mqp6v9zz":"27919523"}}}},"EarnSpendDto":{"type":"object","properties":{"amount":{"type":"number","description":"Spend amount in major currency units, e.g. 24.50 for €24.50 (up to 2 decimals)","example":24.5},"currency":{"type":"string","description":"ISO 4217 currency code"}},"required":["amount","currency"]},"EarnItemDto":{"type":"object","properties":{"key":{"type":"string","description":"Item key configured in the accrual rules"},"quantity":{"type":"number"}},"required":["key","quantity"]},"EarnDto":{"type":"object","properties":{"type":{"type":"string","enum":["points","stamps"]},"amount":{"type":"number","description":"Flat amount to add (points or stamps)"},"from":{"type":"string","enum":["spend","items"]},"spend":{"$ref":"#/components/schemas/EarnSpendDto"},"items":{"type":"array","items":{"$ref":"#/components/schemas/EarnItemDto"}},"note":{"type":"string"}},"required":["type"]},"RedeemDto":{"type":"object","properties":{"type":{"type":"string","enum":["points","reward"],"description":"\"points\" deducts an arbitrary amount (point cards only); \"reward\" redeems the card's fixed configured reward (stamp or point cards)"},"amount":{"type":"number","description":"Arbitrary points amount to deduct, in whole units (store credit may have up to 2 decimals) — only used for type=points. Must be a positive number; required (and must be > 0) when type=points, otherwise a missing/zero amount would silently withdraw real currency on a PREPAID_POINTS card."},"note":{"type":"string"}},"required":["type"]},"AdjustDto":{"type":"object","properties":{"balanceCorrection":{"type":"number","description":"Signed delta applied to the current balance (stamp/point/level cards), in whole units; store credit may have up to 2 decimals. Exactly one of balanceCorrection/setPoints is required. Must be non-zero — a zero delta is a no-op and would silently withdraw 1 unit of real currency on a PREPAID_POINTS card due to a truthiness coercion in LoyaltyService."},"setPoints":{"type":"number","description":"Absolute value to set the points balance to (point/level cards only), in whole units; store credit may have up to 2 decimals. Exactly one of balanceCorrection/setPoints is required. Must be >= 0."},"note":{"type":"string"}}},"UpdateCardIdentityDto":{"type":"object","properties":{"identifierQuestionKey":{"type":"string","nullable":true,"description":"The key of one of this card's dataCollection.fields[] questions to make the card's customer identifier, or null to clear the identifier entirely. The question must be required and use a type that can serve as a stable lookup key (number, text, email or phone). Setting a key on a card that already has a different identifier replaces it in the same write — at most one question is ever the identifier.","example":"custom_mqp6v9zz"}},"required":["identifierQuestionKey"]}}}}