{"openapi":"3.0.3","info":{"title":"Customer App API","version":"1.4.4","description":"First-party customer API. JSON requests and responses unless a response media type says otherwise. Monetary amounts are integer currency minor units; timestamps are ISO 8601. Examples use fictional identifiers and must be replaced. Authentication, entitlements and tenant boundaries are enforced by the server."},"servers":[{"url":"\/api\/app\/v1"}],"security":[{"AppId":[],"Bearer":[]}],"paths":{"\/config":{"get":{"operationId":"getConfig","summary":"Branding, sign-in methods and feature availability","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"business":{"type":"object","properties":{"name":{"type":"string","description":"Display name of the object represented here; do not use it as a stable identifier."},"currency":{"type":"string","description":"ISO 4217 currency code, for example GBP or AED. Monetary amounts are expressed in its minor units."},"locale":{"type":"string","description":"Language\/region code such as en_GB; determines localized presentation."},"timezone":{"type":"string","description":"IANA timezone such as Europe\/London or Asia\/Dubai, used for business-local rules and schedules."}},"description":"Business identity and regional settings for the resolved tenant."},"app":{"type":"object","properties":{"name":{"type":"string","description":"Display name of the object represented here; do not use it as a stable identifier."},"sign_up_methods":{"type":"array","items":{"type":"string","enum":["phone","email"]},"description":"Enabled methods for both sign-in and sign-up. Show only these choices, including for returning customers. Configured phone sign-in is advertised only when a usable SMS verification account is available or this active app has an explicit test-number allow-list with test OTP enabled. A globally advertised phone option does not make every number a test number: without usable SMS, a non-listed number can still return 422 method_not_enabled."},"min_app_version":{"type":"object","properties":{"ios":{"type":"string","description":"Minimum supported iOS app version as major.minor.patch; absent when no minimum is configured."},"android":{"type":"string","description":"Minimum supported Android app version as major.minor.patch; absent when no minimum is configured."}},"description":"Minimum supported app version by platform, or the version required by an upgrade error."},"customer_experience_mode":{"type":"string","enum":["app_only","app_free_only","hybrid"],"description":"Platform-managed customer experience mode. Native customer requests are refused with 403 customer_channel_unavailable for app_free_only. Staff access and installed Wallet validity are independent."}},"description":"Customer app display settings and supported sign-in policy."},"card":{"type":"object","properties":{"title":{"type":"string","description":"Customer-facing title printed on the loyalty card."},"logo_url":{"type":"string","nullable":true,"description":"URL of the business loyalty card logo, when configured."},"color":{"type":"string","description":"Configured loyalty card brand color, for example #6b3e26."},"points_label":{"type":"string","description":"Business-specific display label for points, such as points or beans."},"redemption_model":{"type":"string","description":"Loyalty program model: catalogue exchanges points for rewards; milestones unlocks rewards through card progress."},"earn_rule_label":{"type":"string","nullable":true,"maxLength":60,"description":"Short text set by the business describing how points are earned. Hide the line when null."}},"nullable":true,"description":"Loyalty card branding or milestone progress, depending on this response."},"whatsapp_number":{"type":"string","nullable":true,"description":"Business WhatsApp number in international format, or null when none is configured."},"google_wallet":{"type":"boolean","description":"Whether Google Wallet is available for this business\/customer."},"apple_wallet":{"type":"boolean","description":"Whether Apple Wallet pass downloads are configured for this business."},"input_profile":{"$ref":"#\/components\/schemas\/CountryInputProfile"},"phone_otp":{"$ref":"#\/components\/schemas\/PhoneOtpCountries"},"anonymous":{"type":"object","description":"Additional optional browser enrollment policy. Native anonymous enrollment is not supported.","properties":{"enabled":{"type":"boolean","description":"Whether the business currently accepts anonymous browser joins."},"remember_days":{"type":"integer","description":"Absolute lifetime of anonymous private-browser authority; browser storage may be cleared earlier."}}},"loyalty":{"type":"object","nullable":true,"description":"Current rewards programme availability; null when loyalty is not enabled.","properties":{"available":{"type":"boolean","description":"False while the store has paused the rewards programme; show the paused screen. Null object means loyalty is not enabled."}}},"legal":{"type":"object","description":"Public Terms of Use and Privacy Policy URLs for this business.","properties":{"terms_url":{"type":"string","format":"uri","description":"Public HTTPS Terms of Use page. Always present: the business's hosted page, or its own website when the business chose that."},"privacy_url":{"type":"string","format":"uri","description":"Public HTTPS Privacy Policy page. Always present: the business's hosted page, or its own website when the business chose that."}}}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"security":[{"AppId":[]}],"tags":["Configuration"],"description":"Read branding, enabled sign-in methods, minimum app versions and feature availability before displaying the sign-in screen. A public app ID is required; a customer token is not. Configured phone sign-in is advertised only when the selected phone verification provider is available or this active app has an explicit test-number allow-list with test OTP enabled. A globally advertised phone option does not make every number a test number: without usable SMS, a non-listed number can still return 422 method_not_enabled."}},"\/branches":{"get":{"operationId":"listBranches","summary":"Active business branches","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"branches":{"type":"array","items":{"$ref":"#\/components\/schemas\/Branch"},"description":"Branches included in this response. Follow pagination where supplied rather than assuming the list is complete."},"meta":{"$ref":"#\/components\/schemas\/Meta"}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"security":[{"AppId":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"One-based page number. Omit to request the first page; use meta.last_page to know when to stop.","example":1},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"description":"Number of records requested per page. Defaults to 20; minimum 1 and maximum 50. Use pagination metadata to retrieve further pages.","example":20}],"tags":["Branches"],"description":"List only active branches belonging to the app\u2019s business. Use the pagination metadata to load subsequent pages; per_page is capped at 50."}},"\/branches\/{branch}":{"get":{"operationId":"getBranch","summary":"One active branch","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"branch":{"$ref":"#\/components\/schemas\/Branch"}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"security":[{"AppId":[]}],"parameters":[{"name":"branch","in":"path","required":true,"schema":{"type":"string","minLength":26,"maxLength":26},"description":"Public branch ULID from GET \/branches, not a branch name or legacy integer ID. Inactive, unknown or other-business branches return 404.","example":"01JB0000000000000000000001"}],"tags":["Branches"],"description":"Retrieve one active branch by its stable public ID. Inactive, unknown and other-business branches return not_found."}},"\/auth\/start":{"post":{"operationId":"startSignIn","summary":"Request a sign-in code with exactly one identifier","description":"Start sign-up or sign-in using exactly one email or phone and a stable installation device_id. The response does not reveal whether the account already exists. Code delivery is asynchronous. Configured phone sign-in is advertised only when the selected phone verification provider is available or this active app has an explicit test-number allow-list with test OTP enabled. A globally advertised phone option does not make every number a test number: without usable SMS, a non-listed number can still return 422 method_not_enabled. Generated verification codes normally have 6 digits. Only phone sign-in\/sign-up for explicitly listed numbers in an app with Super Admin-enabled test OTP accepts the fixed string 112233 without sending SMS. The setting is per app, defaults off and does not apply to email, identifier-linking or deletion OTPs. Pending test challenges stop working if test mode is disabled or the number is removed; expiry, attempt limits, one-use consumption and app\/business scoping still apply. Demo access also requires the platform demo switch (DEMO_PHONE_OTP_ENABLED, off by default in production) and an unexpired per-app grant, configured for at most seven days. Demo identifiers remain unverified and cannot reserve a verified phone. Verified customer numbers are excluded; demo accounts cannot link identifiers, merge or purchase\/redeem rewards. These login\/registration requests skip SMS and request proof and never claim provisional accounts. Other numbers and sensitive actions retain proof requirements. Bird Verify returns a challenge_token and a pending send. Poll the matching status operation before asking for a code. Local SMS and email retain their existing check behavior. Ordinary code requests allow 5 per identifier, 20 per IP and 10 per device each hour. Registration for an active app's enabled, unexpired, allowlisted test phone instead allows 30 new challenges per minute per app and number, without consuming or waiting for ordinary hourly budgets. The public API request limit still applies. Verified customer numbers, other numbers, email and non-registration purposes retain normal limits. HTTP 429 supplies retry_after in seconds and a Retry-After header.","responses":{"202":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"challenge_id":{"type":"string","minLength":26,"maxLength":26,"description":"Opaque verification challenge ULID returned by the matching start endpoint; use only for that verification workflow."},"channel":{"type":"string","enum":["email","sms","whatsapp"],"description":"Channel used for this operation; see the endpoint-specific allowed values.","nullable":true},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"provider":{"type":"string","description":"Selected provider; bird_verify requires queued checks.","example":"bird_verify"},"status":{"type":"string","description":"A remote send starts pending.","example":"pending"},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Current code expiry in the business time zone. Before remote send completion this is provisional; poll status for the provider expiry.","example":"2026-10-30T10:22:00+00:00"},"resend_after":{"type":"string","format":"date-time","nullable":true,"description":"Earliest resend time in the business time zone. Null while a remote send is pending, after the send limit, or when the challenge is no longer pending. Poll status after a queued send.","example":"2026-10-30T10:13:00+00:00"},"attempts_allowed":{"type":"integer","description":"Maximum number of code checks allowed for this challenge.","example":5},"attempts_left":{"type":"integer","minimum":0,"description":"Current checks remaining; zero after attempt exhaustion. The final wrong check returns too_many_attempts.","example":4}},"required":["challenge_id","channel"]},"example":{"challenge_id":"01JB0000000000000000000001","channel":"email"}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"security":[{"AppId":[]}],"parameters":[{"name":"X-App-Proof","in":"header","required":false,"schema":{"type":"string"},"description":"Required when proof protection is enabled (default in production). Obtain a fresh, one-use proof bound to this app and device from the trusted server issuer. Missing\/replayed proof returns 403 app_verification_required; never embed the issuer secret in an app. Explicitly enabled apps' exact allowlisted demo phones are exempt for login\/registration only; all other requests require proof when enabled.","example":"YOUR_VERIFICATION_PROOF"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","maxLength":255,"description":"Email address to receive the verification code. Required when phone is absent. Use a method enabled for this app.","example":"customer@example.com"},"phone":{"type":"string","maxLength":30,"description":"Phone number to receive the SMS verification code. Required when email is absent. Use E.164 or a local number valid for the business country.","example":"+447700900123"},"device_id":{"type":"string","maxLength":100,"description":"Stable identifier generated by the app once per installation. Reuse it across sign-in attempts; do not use a new value for each request.","example":"install-demo-001"}},"required":["device_id"],"description":"Supply one contact identifier: email or phone. Sending both is rejected. device_id is required on every sign-in attempt."},"example":{"email":"customer@example.com","device_id":"install-demo-001"}}}},"tags":["Authentication"]}},"\/auth\/verify":{"post":{"operationId":"verifySignIn","summary":"Consume a code and sign this installation in","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/Me"},{"type":"object","properties":{"token":{"type":"string","description":"Bearer credential returned at sign-in. Store securely and send in Authorization: Bearer YOUR_TOKEN; never put it in a URL."}},"required":["token"]}]}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"202":{"description":"Remote verification is queued; no customer authority has been granted.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Pending until the worker records an outcome.","example":"pending"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26},"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"retry_after":{"type":"integer","description":"Seconds to wait before polling.","example":3}}},"example":{"status":"pending","operation_id":"01JB0000000000000000000002","retry_after":3}}}}},"security":[{"AppId":[]}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"challenge_id":{"type":"string","minLength":26,"maxLength":26,"description":"Sign-in challenge ULID returned by POST \/auth\/start for this business app. Login\/register challenges only.","example":"01JB0000000000000000000001"},"code":{"type":"string","maxLength":10,"description":"Verification code delivered for this challenge. Send as a string to preserve leading zeros. Expired, incorrect or consumed codes are refused. Generated verification codes normally have 6 digits. Only phone sign-in\/sign-up for explicitly listed numbers in an app with Super Admin-enabled test OTP accepts the fixed string 112233 without sending SMS. The setting is per app, defaults off and does not apply to email, identifier-linking or deletion OTPs. Pending test challenges stop working if test mode is disabled or the number is removed; expiry, attempt limits, one-use consumption and app\/business scoping still apply. Demo access also requires the platform demo switch (DEMO_PHONE_OTP_ENABLED, off by default in production) and an unexpired per-app grant, configured for at most seven days. Demo identifiers remain unverified and cannot reserve a verified phone. Verified customer numbers are excluded; demo accounts cannot link identifiers, merge or purchase\/redeem rewards. These login\/registration requests skip SMS and request proof and never claim provisional accounts. Other numbers and sensitive actions retain proof requirements.","example":"123456"},"device":{"$ref":"#\/components\/schemas\/Device"},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26}},"required":["challenge_id","code","device"]},"example":{"challenge_id":"01JB0000000000000000000001","code":"123456","device":{"id":"install-demo-001","platform":"ios","name":"Demo iPhone","app_version":"1.0.0"}}}}},"tags":["Authentication"],"description":"Consume the sign-in challenge and issue a token for this installation. Codes expire after 10 minutes; newly issued tokens have a fixed 30-day lifetime. Signing in on the same device replaces its previous token. Generated verification codes normally have 6 digits. Only phone sign-in\/sign-up for explicitly listed numbers in an app with Super Admin-enabled test OTP accepts the fixed string 112233 without sending SMS. The setting is per app, defaults off and does not apply to email, identifier-linking or deletion OTPs. Pending test challenges stop working if test mode is disabled or the number is removed; expiry, attempt limits, one-use consumption and app\/business scoping still apply. Demo access also requires the platform demo switch (DEMO_PHONE_OTP_ENABLED, off by default in production) and an unexpired per-app grant, configured for at most seven days. Demo identifiers remain unverified and cannot reserve a verified phone. Verified customer numbers are excluded; demo accounts cannot link identifiers, merge or purchase\/redeem rewards. These login\/registration requests skip SMS and request proof and never claim provisional accounts. Other numbers and sensitive actions retain proof requirements. A device ID belonging to another customer returns 409 device_in_use without altering that installation or token. Generate a new random device ID and restart sign-in. With Bird Verify, submit challenge_token and an Idempotency-Key. HTTP 202 contains a queued check operation. Poll its status, then repeat this request with the same code\/device, capability and operation_id to finalize locally. GET\/status never returns a session token. A lost finalization response requires a fresh sign-in; completion does not replay a consumed challenge.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Required for a new Bird Verify check\/resend. Reuse for a transport retry with identical input; use a new key for a different code.","example":"verification-check-001","schema":{"type":"string","maxLength":100}}]}},"\/auth\/logout":{"post":{"operationId":"logout","summary":"Revoke this token","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"signed_out":{"type":"boolean","description":"True after the current device token has been revoked. Clear the locally stored token."}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"tags":["Authentication"],"description":"Revoke the current installation token. Remove the token from secure device storage after success."}},"\/devices\/push-token":{"put":{"operationId":"setPushToken","summary":"Register or remove this installation push token","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"push":{"type":"boolean","description":"Whether a non-null push token is registered by this update."}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"push_token":{"type":"string","maxLength":500,"nullable":true,"description":"FCM registration token for this installation. Send null to remove it and stop push delivery to this device.","example":null},"firebase_project_id":{"type":"string","maxLength":100,"nullable":true,"description":"Firebase project ID from this app build's own Firebase options. Required with a non-null push_token on a device that has no registered project yet; an older build may omit it only to refresh the token of a device already registered. 409 push_project_mismatch when the business has not set up this project for the app; 409 push_project_required when it is missing on a new device.","example":"cafe-app-prod"}},"required":["push_token"]},"example":{"push_token":"fcm\u2026","firebase_project_id":"cafe-app-prod"}}}},"tags":["Devices"],"description":"Register the FCM token of the signed-in installation together with the Firebase project it came from, or pass null to stop push to this device. Device registration alone is not marketing consent."}},"\/me":{"get":{"operationId":"getMe","summary":"Account, available rewards and card","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Me"}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"parameters":[{"name":"X-App-Platform","in":"header","required":false,"schema":{"type":"string","enum":["ios","android"]},"description":"Installed native platform, ios or android. Used with X-App-Version for minimum-version checks; if omitted, the signed-in installation value is used.","example":"ios"},{"name":"X-App-Version","in":"header","required":false,"schema":{"type":"string"},"description":"Installed app version, for example 1.4.0. Send after upgrading an existing installation. Missing or older versions return 426 when a minimum applies; omitted uses the signed-in installation value.","example":"1.4.0"}],"tags":["Account"],"description":"Read the signed-in customer profile, membership and up to 50 available rewards. Use \/me\/rewards for paginated reward history."},"patch":{"operationId":"updateMe","summary":"Profile and separately recorded consent","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Me"}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"display_name":{"type":"string","maxLength":100,"nullable":true,"description":"Customer display name. Send null to clear it; omitted fields are left unchanged.","example":"Alex Morgan"},"name":{"type":"string","maxLength":100,"nullable":true,"description":"Alias for display_name. If both fields are provided, display_name wins. Null clears the stored name."},"locale":{"type":"string","maxLength":10,"nullable":true,"description":"Customer locale such as en, en_GB or en-GB: two lowercase language letters, optionally followed by an underscore\/hyphen and two region letters. Null clears it.","example":"en_GB"},"birthday":{"type":"string","nullable":true,"description":"Full date of birth as YYYY-MM-DD, for example 1995-10-24. Must be a valid calendar date, no later than today in the business timezone. Null clears it.","example":"1995-10-24","format":"date"},"push_consent":{"type":"boolean","description":"Customer consent for push marketing. Marketing delivery additionally requires marketing_consent and a reachable signed-in device.","example":true},"marketing_consent":{"type":"boolean","description":"Customer consent for marketing messages. Marketing push also requires push_consent; a push token alone is not consent.","example":false}},"description":"Partial profile update. Omitted fields remain unchanged; nullable fields may be cleared with null. Consent changes must reflect the customer\u2019s explicit choice."},"example":{"display_name":"Alex Morgan","locale":"en_GB","birthday":"1995-10-24","push_consent":true,"marketing_consent":false}}}},"tags":["Account"],"description":"Update profile and explicit consent. name aliases display_name; display_name takes precedence. Birthday is a full date of birth (YYYY-MM-DD); consent flags are independently recorded."},"delete":{"operationId":"deleteMe","summary":"Erase the app account, revoke tokens, queue snapshot cleanup","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","description":"True after account erasure has started and the account tokens have been revoked. Some cleanup completes asynchronously."}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"202":{"description":"Remote verification is queued; no customer authority has been granted.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Pending until the worker records an outcome.","example":"pending"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26},"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"retry_after":{"type":"integer","description":"Seconds to wait before polling.","example":3}}},"example":{"status":"pending","operation_id":"01JB0000000000000000000002","retry_after":3}}}}},"description":"Erase the app account with a fresh, single-use deletion OTP from POST \/me\/deletion\/start. Revokes tokens immediately and queues retained snapshot cleanup. Financial audit records remain for reconciliation. With Bird Verify, submit challenge_token and an Idempotency-Key. HTTP 202 contains a queued check operation. Poll its status, then repeat this request with the same code\/device, capability and operation_id to finalize locally. GET\/status never returns a session token. A lost finalization response requires a fresh sign-in; completion does not replay a consumed challenge.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["challenge_id","code"],"properties":{"challenge_id":{"type":"string","minLength":26,"maxLength":26,"description":"Dedicated deletion challenge ULID from POST \/me\/deletion\/start for this signed-in customer. Login and identifier-link challenges cannot delete an account.","example":"01JB0000000000000000000001"},"code":{"type":"string","description":"Verification code delivered for this challenge. Send as a string to preserve leading zeros. Expired, incorrect or consumed codes are refused.","example":"123456","maxLength":10},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26}}},"example":{"challenge_id":"01JB0000000000000000000001","code":"123456"}}}},"tags":["Account"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Required for a new Bird Verify check\/resend. Reuse for a transport retry with identical input; use a new key for a different code.","example":"verification-check-001","schema":{"type":"string","maxLength":100}}]}},"\/me\/history":{"get":{"operationId":"getHistory","summary":"Own points history","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/History"}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"One-based page number. Omit to request the first page; use meta.last_page to know when to stop.","example":1},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"description":"Number of records requested per page. Defaults to 20; minimum 1 and maximum 50. Use pagination metadata to retrieve further pages.","example":20}],"tags":["Account"],"description":"Paginated history for this customer only. Monetary values are minor units; points and balances are integers. Page size is capped at 50."}},"\/me\/rewards":{"get":{"operationId":"getOwnRewards","summary":"Own reward grants in all lifecycle states","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"rewards":{"type":"array","items":{"$ref":"#\/components\/schemas\/Grant"},"description":"Reward catalogue entries or customer reward grants, as defined by the item schema for this endpoint."},"meta":{"$ref":"#\/components\/schemas\/Meta"}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"One-based page number. Omit to request the first page; use meta.last_page to know when to stop.","example":1},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"description":"Number of records requested per page. Defaults to 20; minimum 1 and maximum 50. Use pagination metadata to retrieve further pages.","example":20},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["earned","available","used","expired","revoked"]},"description":"Filter owned reward grants by lifecycle state. Omit to include all states. Expired available grants are reported as expired even before background expiry processing.","example":"available"}],"tags":["Rewards"],"description":"List this customer\u2019s owned rewards, optionally filtered by earned, available, used, expired or revoked. Earned entitlements have no code: use server claim.available_actions and POST rewards\/create-voucher before showing a code. Use this endpoint for full paginated discovery rather than the limited account rewards list."}},"\/rewards":{"get":{"operationId":"getRewardCatalogue","summary":"Active reward catalogue","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"rewards":{"type":"array","items":{"$ref":"#\/components\/schemas\/Reward"},"description":"Reward catalogue entries or customer reward grants, as defined by the item schema for this endpoint."},"meta":{"$ref":"#\/components\/schemas\/Meta"}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"One-based page number. Omit to request the first page; use meta.last_page to know when to stop.","example":1},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"description":"Number of records requested per page. Defaults to 20; minimum 1 and maximum 50. Use pagination metadata to retrieve further pages.","example":20}],"tags":["Rewards"],"description":"Browse the active points-purchase catalogue. Milestone programs issue rewards automatically; catalogue availability depends on the published business policy."}},"\/rewards\/purchase":{"post":{"operationId":"purchaseReward","summary":"Buy one reward for points","responses":{"201":{"description":"Success","content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/Me"},{"type":"object","properties":{"reward":{"$ref":"#\/components\/schemas\/Grant"}}}]}}}},"400":{"$ref":"#\/components\/responses\/Error400"},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":64},"description":"Client-generated stable key for one intended mutation, at most 64 characters. Save it before sending; retry an identical request with the same key after a timeout. Changing the payload with that key returns 409 idempotency_conflict.","example":"demo-operation-001"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"reward":{"type":"string","minLength":26,"maxLength":26,"description":"Public catalogue reward ULID from GET \/rewards. Do not send an issued grant ID or redemption code. The operation spends points to issue a customer reward.","example":"01JB0000000000000000000001"}},"required":["reward"]},"example":{"reward":"01JB0000000000000000000001"}}}},"tags":["Rewards"],"description":"Buy a catalogue reward with spendable points and return the updated account snapshot. Send an Idempotency-Key and retain it when retrying the identical purchase. Eligibility is authoritative on the server. Catalogue claims respect allow_catalogue_with_milestones. Staff-assisted claims require store_reward_claiming_enabled as well as current actor\/branch permission; customer claims and redemption of issued vouchers are independent. Catalogue points are deducted at creation without a separate Customer App approval, per product policy. Sharing the private code separately authorizes redemption."}},"\/wallet":{"get":{"operationId":"getGoogleWallet","summary":"Direct Google Wallet save URL and Android save JWT","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"google_wallet_url":{"type":"string","format":"uri","description":"Direct Google Wallet save URL containing a signed JWT with member information. Never log or publish the URL."},"google_wallet_jwt":{"type":"string","description":"Signed save JWT for Android's PayClient.savePassesJwt(). Present while a Google Wallet card can be issued. Treat as sensitive: it carries the member code."}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"description":"wallet_unavailable: the pass cannot be prepared right now; try again shortly.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"wallet_unavailable","message":"Your Wallet pass cannot be prepared right now. Please try again shortly."}}}}}},"tags":["Wallet & channels"],"description":"Get a private signed URL that opens Google's save flow directly, without an intermediate wallet page. google_wallet_jwt is the same card as a JWT for Android's PayClient.savePassesJwt(). Both carry member information including the member code; never log or publish either value. Returns 404 wallet_unavailable when Google Wallet is unavailable, or 503 wallet_unavailable if the pass cannot be prepared right now."}},"\/wallet\/passes":{"get":{"operationId":"listWalletPasses","summary":"The customer's current wallet passes of every kind, with their live state","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"One-based page number. Omit to request the first page; use meta.last_page to know when to stop.","example":1},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"description":"Number of records requested per page. Defaults to 20; minimum 1 and maximum 50. Use pagination metadata to retrieve further pages.","example":20}],"responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"passes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Stable public ID of the pass."},"kind":{"type":"string","description":"Pass kind, e.g. loyalty (the loyalty card)."},"platform":{"type":"string","enum":["google","apple"],"description":"Wallet the pass belongs to."},"state":{"type":"string","enum":["active","inactive","expired","completed","revoked"],"description":"Live state of the pass. Only active passes are usable; the others stay installed but show as not valid."},"apple":{"type":"object","properties":{"pass_type_identifier":{"type":"string","description":"The pass's Pass Type ID. The app can read it only when listed in its pass-type-identifiers entitlement (same signing team)."},"serial_number":{"type":"string","description":"The pass's serial number; never changes."}},"description":"Present for Apple passes: the identity PKPassLibrary looks passes up by."},"google":{"type":"object","properties":{"object_id":{"type":"string","description":"The Google Wallet object ID; never changes."}},"description":"Present for Google passes."}}},"description":"Current passes of every kind issued to this customer, oldest first."},"meta":{"$ref":"#\/components\/schemas\/Meta"}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"tags":["Wallet & channels"],"description":"The customer's current passes of every kind. The set and total are stable across pages; each pass's state is computed when the page is read, so only show passes whose state is active as usable. Issuing is not installing: a pass listed here may not be on the device. On iOS, check with PKPassLibrary using pass_type_identifier and serial_number; this works only for Pass Type IDs in the app's com.apple.developer.pass-type-identifiers entitlement, which covers the app's own signing team only. Treat an unknown result as not added."}},"\/me\/card\/reissue":{"post":{"operationId":"reissueMemberCard","summary":"Issue a new member code; old code and wallet links stop working","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"member_code":{"type":"string","description":"The member's new 11-digit member code. Staff scan it to spend points; show it only to the signed-in member."}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"tags":["Wallet & channels"],"description":"For a card that was shared or photographed. Generates a new member code, updates Google and Apple Wallet cards to show it, and ends the old code and every wallet link made before. Limited to 3 calls an hour; returns loyalty_unavailable while loyalty is off."}},"\/wallet\/apple":{"get":{"operationId":"getAppleWallet","summary":"Signed Apple Wallet pass with update support","responses":{"200":{"description":"Success","content":{"application\/vnd.apple.pkpass":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"tags":["Wallet & channels"],"description":"Download a signed Apple Wallet pass, not JSON. Stable pass identity and per-pass authentication support queued automatic updates through the separate PassKit service. Returns wallet_unavailable until signing assets are provisioned. Device refresh depends on Apple delivery."}},"\/identifiers\/start":{"post":{"operationId":"startIdentifier","summary":"Verify an additional phone or email","responses":{"202":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"challenge_id":{"type":"string","minLength":26,"maxLength":26,"description":"Opaque verification challenge ULID returned by the matching start endpoint; use only for that verification workflow."},"channel":{"type":"string","enum":["email","sms","whatsapp"],"description":"Channel used for this operation; see the endpoint-specific allowed values.","nullable":true},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"provider":{"type":"string","description":"Selected provider; bird_verify requires queued checks.","example":"bird_verify"},"status":{"type":"string","description":"A remote send starts pending.","example":"pending"},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Current code expiry in the business time zone. Before remote send completion this is provisional; poll status for the provider expiry.","example":"2026-10-30T10:22:00+00:00"},"resend_after":{"type":"string","format":"date-time","nullable":true,"description":"Earliest resend time in the business time zone. Null while a remote send is pending, after the send limit, or when the challenge is no longer pending. Poll status after a queued send.","example":"2026-10-30T10:13:00+00:00"},"attempts_allowed":{"type":"integer","description":"Maximum number of code checks allowed for this challenge.","example":5},"attempts_left":{"type":"integer","minimum":0,"description":"Current checks remaining; zero after attempt exhaustion. The final wrong check returns too_many_attempts.","example":4}},"required":["challenge_id","channel"]},"example":{"challenge_id":"01JB0000000000000000000001","channel":"email"}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","maxLength":255,"description":"Email address to receive the verification code. Required when phone is absent. Use a method enabled for this app.","example":"customer@example.com"},"phone":{"type":"string","maxLength":30,"description":"Phone number to receive the SMS verification code. Required when email is absent. Use E.164 or a local number valid for the business country.","example":"+447700900123"}},"description":"Supply one contact identifier: email or phone. Send only the identifier you want to add to the signed-in customer; if both are provided, email takes precedence."},"example":{"phone":"+447700900123"}}}},"tags":["Identity"],"description":"Request verification of exactly one additional email or phone on the signed-in account. Requires a fresh X-App-Proof bound to the signed-in installation when proof protection is enabled. Authenticated code requests share a 10\/hour customer limit across devices. Bird Verify returns a challenge_token and a pending send. Poll the matching status operation before asking for a code. Local SMS and email retain their existing check behavior.","parameters":[{"name":"X-App-Proof","in":"header","required":false,"schema":{"type":"string"},"description":"Required when proof protection is enabled (default in production). Obtain a fresh, one-use proof bound to this app and device from the trusted server issuer. Missing\/replayed proof returns 403 app_verification_required; never embed the issuer secret in an app.","example":"YOUR_VERIFICATION_PROOF"}]}},"\/identifiers\/verify":{"post":{"operationId":"verifyIdentifier","summary":"Add a verified identifier; may offer a merge challenge","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Me"}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"202":{"description":"Remote verification is queued; no customer authority has been granted.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Pending until the worker records an outcome.","example":"pending"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26},"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"retry_after":{"type":"integer","description":"Seconds to wait before polling.","example":3}}},"example":{"status":"pending","operation_id":"01JB0000000000000000000002","retry_after":3}}}}},"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"challenge_id":{"type":"string","minLength":26,"maxLength":26,"description":"Add-identifier challenge ULID returned by POST \/identifiers\/start for this signed-in customer.","example":"01JB0000000000000000000001"},"code":{"type":"string","description":"Verification code delivered for this challenge. Send as a string to preserve leading zeros. Expired, incorrect or consumed codes are refused.","example":"123456","maxLength":10},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26}},"required":["challenge_id","code"]},"example":{"challenge_id":"01JB0000000000000000000001","code":"123456"}}}},"tags":["Identity"],"description":"Verify an additional identifier. If another account holds it, handle held_by_another_account and ask the customer before submitting the returned merge challenge. With Bird Verify, submit challenge_token and an Idempotency-Key. HTTP 202 contains a queued check operation. Poll its status, then repeat this request with the same code\/device, capability and operation_id to finalize locally. GET\/status never returns a session token. A lost finalization response requires a fresh sign-in; completion does not replay a consumed challenge.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Required for a new Bird Verify check\/resend. Reuse for a transport retry with identical input; use a new key for a different code.","example":"verification-check-001","schema":{"type":"string","maxLength":100}}]}},"\/accounts\/merge":{"post":{"operationId":"mergeAccount","summary":"Merge another account after verifying its identifier","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Me"}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"challenge_id":{"type":"string","minLength":26,"maxLength":26,"description":"Merge challenge ULID from error.merge_challenge_id when POST \/identifiers\/verify returns held_by_another_account. Obtain explicit customer agreement before merging.","example":"01JB0000000000000000000001"}},"required":["challenge_id"]},"example":{"challenge_id":"01JB0000000000000000000001"}}}},"tags":["Identity"],"description":"Consume the merge challenge after explicit customer confirmation. Move supported account data into the signed-in account and revoke the merged account\u2019s devices. Challenge validity is 15 minutes."}},"\/whatsapp\/link":{"post":{"operationId":"linkWhatsApp","summary":"Existing optional WhatsApp linking endpoint","responses":{"201":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Private WhatsApp linking URL. Open it for the signed-in customer before expires_at; it contains a short-lived linking code."},"expires_at":{"type":"string","format":"date-time","description":"ISO 8601 expiration timestamp, or null if no expiry applies. Do not offer an expired link or reward for use."}}}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"tags":["Wallet & channels"],"description":"Create a one-use WhatsApp linking URL valid for 15 minutes. The customer must send the prefilled message to complete linking; creating the URL alone does not link the account. Sending the prefilled message only requests confirmation. The recipient must tap the WhatsApp confirmation button naming the app account before any account linking. The app-visible token cannot confirm; a new single-use secret is bound to the WhatsApp recipient."}},"\/me\/deletion\/start":{"post":{"summary":"Request an account deletion OTP","requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"method":{"type":"string","enum":["phone","email"],"description":"Verified identifier type used to deliver the account-deletion code. If omitted, a verified phone is preferred, then email.","example":"email"}}},"example":{"method":"email"}}}},"responses":{"202":{"description":"OTP queued to an existing verified identifier","content":{"application\/json":{"schema":{"type":"object","required":["challenge_id","channel"],"properties":{"challenge_id":{"type":"string","minLength":26,"maxLength":26,"description":"Opaque verification challenge ULID returned by the matching start endpoint; use only for that verification workflow."},"channel":{"type":"string","enum":["email","sms","whatsapp"],"description":"Channel used for this operation; see the endpoint-specific allowed values.","nullable":true},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"provider":{"type":"string","description":"Selected provider; bird_verify requires queued checks.","example":"bird_verify"},"status":{"type":"string","description":"A remote send starts pending.","example":"pending"},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Current code expiry in the business time zone. Before remote send completion this is provisional; poll status for the provider expiry.","example":"2026-10-30T10:22:00+00:00"},"resend_after":{"type":"string","format":"date-time","nullable":true,"description":"Earliest resend time in the business time zone. Null while a remote send is pending, after the send limit, or when the challenge is no longer pending. Poll status after a queued send.","example":"2026-10-30T10:13:00+00:00"},"attempts_allowed":{"type":"integer","description":"Maximum number of code checks allowed for this challenge.","example":5},"attempts_left":{"type":"integer","minimum":0,"description":"Current checks remaining; zero after attempt exhaustion. The final wrong check returns too_many_attempts.","example":4}}},"example":{"challenge_id":"01JB0000000000000000000001","channel":"email"}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"429":{"$ref":"#\/components\/responses\/Error429"}},"operationId":"startAccountDeletion","tags":["Account"],"description":"Send a dedicated account-deletion OTP to an existing verified identifier. Pass the resulting challenge_id and code to DELETE \/me; sign-in challenges cannot authorize erasure. Requires a fresh X-App-Proof bound to the signed-in installation when proof protection is enabled. Authenticated code requests share a 10\/hour customer limit across devices. Bird Verify returns a challenge_token and a pending send. Poll the matching status operation before asking for a code. Local SMS and email retain their existing check behavior.","parameters":[{"name":"X-App-Proof","in":"header","required":false,"schema":{"type":"string"},"description":"Required when proof protection is enabled (default in production). Obtain a fresh, one-use proof bound to this app and device from the trusted server issuer. Missing\/replayed proof returns 403 app_verification_required; never embed the issuer secret in an app.","example":"YOUR_VERIFICATION_PROOF"}]}},"\/rewards\/create-voucher":{"post":{"operationId":"createEarnedRewardVoucher","summary":"Create the single voucher for an earned milestone reward","responses":{"201":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"reward":{"$ref":"#\/components\/schemas\/Grant"}}}}}},"400":{"$ref":"#\/components\/responses\/Error400"},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":64},"description":"Client-generated stable key for one intended mutation, at most 64 characters. Save it before sending; retry an identical request with the same key after a timeout. Changing the payload with that key returns 409 idempotency_conflict.","example":"demo-operation-001"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"entitlement":{"type":"string","minLength":26,"maxLength":26,"description":"Public earned customer reward grant ULID, from owned rewards or staff lookup; not a catalogue reward ID.","example":"01JB0000000000000000000001"}},"required":["entitlement"]},"example":{"entitlement":"01JB0000000000000000000001"}}}},"tags":["Rewards"],"description":"Claim an earned entitlement in place. Both customer and store creation share the same locked record and cannot issue duplicate vouchers. Requires an active verified customer, active membership\/program and unexpired, unrevoked eligibility. No additional milestone point deduction; expiry remains measured from earning. This does not redeem the voucher. The signed-in customer must own the entitlement. Returns the voucher code, including when the store already created it. Reusing an idempotency key for a different entitlement returns 409. Eligibility is authoritative on the server. Catalogue claims respect allow_catalogue_with_milestones. Staff-assisted claims require store_reward_claiming_enabled as well as current actor\/branch permission; customer claims and redemption of issued vouchers are independent. Catalogue points are deducted at creation without a separate Customer App approval, per product policy. Sharing the private code separately authorizes redemption."}},"\/me\/activity":{"get":{"operationId":"getActivity","summary":"Own points and immutable voucher-use activity","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Activity"}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"}},"parameters":[{"name":"filter","in":"query","schema":{"type":"string","enum":["all","earned","used"],"default":"all"},"description":"earned selects positive posted earn entries; used selects actual voucher uses.","example":"all"},{"name":"per_page","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"description":"Number of events per page, from 1 to 50; default 20.","example":20},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":4096},"description":"Opaque meta.next_cursor, omitted for the first page. URL-encode it.","example":"opaque-cursor-from-meta"}],"tags":["Account"],"description":"Cursor-paginated customer activity. redeem is catalogue creation; reward_used is actual voucher use with null points\/balance_after. Voids remain visible in all. Filters apply before pagination. Cursors bind to this customer and filter; invalid cursors return 422 invalid_cursor. Loyalty required."}},"\/auth\/complete":{"post":{"operationId":"completeSignIn","summary":"Finalize a successful queued sign-in check","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"allOf":[{"$ref":"#\/components\/schemas\/Me"},{"type":"object","properties":{"token":{"type":"string","description":"Bearer credential returned at sign-in. Store securely and send in Authorization: Bearer YOUR_TOKEN; never put it in a URL."}},"required":["token"]}]}}}},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"202":{"description":"Remote verification is queued; no customer authority has been granted.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Pending until the worker records an outcome.","example":"pending"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26},"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"retry_after":{"type":"integer","description":"Seconds to wait before polling.","example":3}}},"example":{"status":"pending","operation_id":"01JB0000000000000000000002","retry_after":3}}}}},"security":[{"AppId":[]}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"challenge_id":{"type":"string","minLength":26,"maxLength":26,"description":"Sign-in challenge ULID returned by POST \/auth\/start for this business app. Login\/register challenges only.","example":"01JB0000000000000000000001"},"code":{"type":"string","maxLength":10,"description":"Verification code delivered for this challenge. Send as a string to preserve leading zeros. Expired, incorrect or consumed codes are refused. Generated verification codes normally have 6 digits. Only phone sign-in\/sign-up for explicitly listed numbers in an app with Super Admin-enabled test OTP accepts the fixed string 112233 without sending SMS. The setting is per app, defaults off and does not apply to email, identifier-linking or deletion OTPs. Pending test challenges stop working if test mode is disabled or the number is removed; expiry, attempt limits, one-use consumption and app\/business scoping still apply. Demo access also requires the platform demo switch (DEMO_PHONE_OTP_ENABLED, off by default in production) and an unexpired per-app grant, configured for at most seven days. Demo identifiers remain unverified and cannot reserve a verified phone. Verified customer numbers are excluded; demo accounts cannot link identifiers, merge or purchase\/redeem rewards. These login\/registration requests skip SMS and request proof and never claim provisional accounts. Other numbers and sensitive actions retain proof requirements.","example":"123456"},"device":{"$ref":"#\/components\/schemas\/Device"},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26}},"required":["challenge_id","code","device"]},"example":{"challenge_id":"01JB0000000000000000000001","code":"123456","device":{"id":"install-demo-001","platform":"ios","name":"Demo iPhone","app_version":"1.0.0"}}}}},"tags":["Authentication"],"description":"Consume the sign-in challenge and issue a token for this installation. Codes expire after 10 minutes; newly issued tokens have a fixed 30-day lifetime. Signing in on the same device replaces its previous token. Generated verification codes normally have 6 digits. Only phone sign-in\/sign-up for explicitly listed numbers in an app with Super Admin-enabled test OTP accepts the fixed string 112233 without sending SMS. The setting is per app, defaults off and does not apply to email, identifier-linking or deletion OTPs. Pending test challenges stop working if test mode is disabled or the number is removed; expiry, attempt limits, one-use consumption and app\/business scoping still apply. Demo access also requires the platform demo switch (DEMO_PHONE_OTP_ENABLED, off by default in production) and an unexpired per-app grant, configured for at most seven days. Demo identifiers remain unverified and cannot reserve a verified phone. Verified customer numbers are excluded; demo accounts cannot link identifiers, merge or purchase\/redeem rewards. These login\/registration requests skip SMS and request proof and never claim provisional accounts. Other numbers and sensitive actions retain proof requirements. A device ID belonging to another customer returns 409 device_in_use without altering that installation or token. Generate a new random device ID and restart sign-in. With Bird Verify, submit challenge_token and an Idempotency-Key. HTTP 202 contains a queued check operation. Poll its status, then repeat this request with the same code\/device, capability and operation_id to finalize locally. GET\/status never returns a session token. A lost finalization response requires a fresh sign-in; completion does not replay a consumed challenge.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Required for a new Bird Verify check\/resend. Reuse for a transport retry with identical input; use a new key for a different code.","example":"verification-check-001","schema":{"type":"string","maxLength":100}}]}},"\/auth\/status":{"post":{"operationId":"auth_status","summary":"Status login or registration verification","description":"Read local worker state without provider HTTP or customer-side effects. Uses only the original business, app, device and caller capability. ","tags":["Authentication"],"responses":{"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"200":{"description":"Local state only. No code or session token is returned.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Pending until the worker records an outcome.","example":"pending"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26},"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"retry_after":{"type":"integer","description":"Seconds to wait before polling.","example":3},"outcome":{"type":"string","description":"sent, verified, invalid_code, expired, too_many_attempts or a safe failure reason.","example":"sent","nullable":true},"channel":{"type":"string","description":"Most recently observed delivery channel.","example":"whatsapp","nullable":true},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Current code expiry in the business time zone. Before remote send completion this is provisional; poll status for the provider expiry.","example":"2026-10-30T10:22:00+00:00"},"resend_after":{"type":"string","format":"date-time","nullable":true,"description":"Earliest resend time in the business time zone. Null while a remote send is pending, after the send limit, or when the challenge is no longer pending. Poll status after a queued send.","example":"2026-10-30T10:13:00+00:00"},"attempts_allowed":{"type":"integer","description":"Maximum number of code checks allowed for this challenge.","example":5},"attempts_left":{"type":"integer","minimum":0,"description":"Current checks remaining; zero after attempt exhaustion. The final wrong check returns too_many_attempts.","example":4},"error":{"type":"object","nullable":true,"description":"Public code-check error for a completed unsuccessful check. Status polling remains HTTP 200; null for pending work and successful sends\/checks.","properties":{"code":{"type":"string","description":"Same public code used by finalization: invalid_code, code_expired, too_many_attempts or challenge_not_found.","example":"invalid_code"},"message":{"type":"string","description":"Human-readable explanation of the check failure.","example":"That code is not right."},"attempts_left":{"type":"integer","minimum":0,"description":"Checks remaining, included for invalid_code.","example":4}}}}},"example":{"status":"pending","operation_id":"01JB0000000000000000000002","retry_after":3,"challenge_id":"01JB0000000000000000000001","expires_at":"2026-10-30T10:22:00+00:00","resend_after":null,"attempts_allowed":5,"attempts_left":5,"error":null}}}}},"parameters":[],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["challenge_id"],"properties":{"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"device_id":{"type":"string","description":"Original installation identifier for public login operations; authenticated operations use their bound installation.","example":"install-demo-001"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26}}},"example":{"challenge_id":"01JB0000000000000000000001","challenge_token":"YOUR_CHALLENGE_TOKEN","device_id":"install-demo-001"}}}},"security":[{"AppId":[]}]}},"\/auth\/resend":{"post":{"operationId":"auth_resend","summary":"Resend login or registration verification","description":"Queue an explicit resend or next-channel request. Subject to local allowance, ownership and cooldown. Never changes provider. Uses only the original business, app, device and caller capability. ","tags":["Authentication"],"responses":{"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"202":{"description":"Remote verification is queued; no customer authority has been granted.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Pending until the worker records an outcome.","example":"pending"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26},"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"retry_after":{"type":"integer","description":"Seconds to wait before polling.","example":3},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Current code expiry in the business time zone. Before remote send completion this is provisional; poll status for the provider expiry.","example":"2026-10-30T10:22:00+00:00"},"resend_after":{"type":"string","format":"date-time","nullable":true,"description":"Earliest resend time in the business time zone. Null while a remote send is pending, after the send limit, or when the challenge is no longer pending. Poll status after a queued send.","example":"2026-10-30T10:13:00+00:00"},"attempts_allowed":{"type":"integer","description":"Maximum number of code checks allowed for this challenge.","example":5},"attempts_left":{"type":"integer","minimum":0,"description":"Current checks remaining; zero after attempt exhaustion. The final wrong check returns too_many_attempts.","example":4},"channel":{"type":"string","nullable":true,"description":"Most recently observed delivery channel; poll status for the newly queued send.","example":"sms"}}},"example":{"status":"pending","operation_id":"01JB0000000000000000000002","retry_after":3,"challenge_id":"01JB0000000000000000000001","expires_at":"2026-10-30T10:22:00+00:00","resend_after":null,"attempts_allowed":5,"attempts_left":5}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Required for a new Bird Verify check\/resend. Reuse for a transport retry with identical input; use a new key for a different code.","example":"verification-check-001","schema":{"type":"string","maxLength":100}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["challenge_id"],"properties":{"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"device_id":{"type":"string","description":"Original installation identifier for public login operations; authenticated operations use their bound installation.","example":"install-demo-001"},"next_channel":{"type":"boolean","description":"Explicitly advance Bird to its next allowed delivery channel. Earlier codes can remain valid. Unsupported for local SMS.","example":false}}},"example":{"challenge_id":"01JB0000000000000000000001","challenge_token":"YOUR_CHALLENGE_TOKEN","device_id":"install-demo-001"}}}},"security":[{"AppId":[]}]}},"\/verification\/status":{"post":{"operationId":"verification_status","summary":"Status identifier linking or browser reauthentication verification","description":"Read local worker state without provider HTTP or customer-side effects. Uses only the original business, app, device and caller capability. ","tags":["Identity"],"responses":{"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"200":{"description":"Local state only. No code or session token is returned.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Pending until the worker records an outcome.","example":"pending"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26},"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"retry_after":{"type":"integer","description":"Seconds to wait before polling.","example":3},"outcome":{"type":"string","description":"sent, verified, invalid_code, expired, too_many_attempts or a safe failure reason.","example":"sent","nullable":true},"channel":{"type":"string","description":"Most recently observed delivery channel.","example":"whatsapp","nullable":true},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Current code expiry in the business time zone. Before remote send completion this is provisional; poll status for the provider expiry.","example":"2026-10-30T10:22:00+00:00"},"resend_after":{"type":"string","format":"date-time","nullable":true,"description":"Earliest resend time in the business time zone. Null while a remote send is pending, after the send limit, or when the challenge is no longer pending. Poll status after a queued send.","example":"2026-10-30T10:13:00+00:00"},"attempts_allowed":{"type":"integer","description":"Maximum number of code checks allowed for this challenge.","example":5},"attempts_left":{"type":"integer","minimum":0,"description":"Current checks remaining; zero after attempt exhaustion. The final wrong check returns too_many_attempts.","example":4},"error":{"type":"object","nullable":true,"description":"Public code-check error for a completed unsuccessful check. Status polling remains HTTP 200; null for pending work and successful sends\/checks.","properties":{"code":{"type":"string","description":"Same public code used by finalization: invalid_code, code_expired, too_many_attempts or challenge_not_found.","example":"invalid_code"},"message":{"type":"string","description":"Human-readable explanation of the check failure.","example":"That code is not right."},"attempts_left":{"type":"integer","minimum":0,"description":"Checks remaining, included for invalid_code.","example":4}}}}},"example":{"status":"pending","operation_id":"01JB0000000000000000000002","retry_after":3,"challenge_id":"01JB0000000000000000000001","expires_at":"2026-10-30T10:22:00+00:00","resend_after":null,"attempts_allowed":5,"attempts_left":5,"error":null}}}}},"parameters":[],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["challenge_id"],"properties":{"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"device_id":{"type":"string","description":"Original installation identifier for public login operations; authenticated operations use their bound installation.","example":"install-demo-001"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26}}},"example":{"challenge_id":"01JB0000000000000000000001","challenge_token":"YOUR_CHALLENGE_TOKEN","device_id":"install-demo-001"}}}}}},"\/verification\/resend":{"post":{"operationId":"verification_resend","summary":"Resend identifier linking or browser reauthentication verification","description":"Queue an explicit resend or next-channel request. Subject to local allowance, ownership and cooldown. Never changes provider. Uses only the original business, app, device and caller capability. ","tags":["Identity"],"responses":{"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"202":{"description":"Remote verification is queued; no customer authority has been granted.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Pending until the worker records an outcome.","example":"pending"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26},"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"retry_after":{"type":"integer","description":"Seconds to wait before polling.","example":3},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Current code expiry in the business time zone. Before remote send completion this is provisional; poll status for the provider expiry.","example":"2026-10-30T10:22:00+00:00"},"resend_after":{"type":"string","format":"date-time","nullable":true,"description":"Earliest resend time in the business time zone. Null while a remote send is pending, after the send limit, or when the challenge is no longer pending. Poll status after a queued send.","example":"2026-10-30T10:13:00+00:00"},"attempts_allowed":{"type":"integer","description":"Maximum number of code checks allowed for this challenge.","example":5},"attempts_left":{"type":"integer","minimum":0,"description":"Current checks remaining; zero after attempt exhaustion. The final wrong check returns too_many_attempts.","example":4},"channel":{"type":"string","nullable":true,"description":"Most recently observed delivery channel; poll status for the newly queued send.","example":"sms"}}},"example":{"status":"pending","operation_id":"01JB0000000000000000000002","retry_after":3,"challenge_id":"01JB0000000000000000000001","expires_at":"2026-10-30T10:22:00+00:00","resend_after":null,"attempts_allowed":5,"attempts_left":5}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Required for a new Bird Verify check\/resend. Reuse for a transport retry with identical input; use a new key for a different code.","example":"verification-check-001","schema":{"type":"string","maxLength":100}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["challenge_id"],"properties":{"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"device_id":{"type":"string","description":"Original installation identifier for public login operations; authenticated operations use their bound installation.","example":"install-demo-001"},"next_channel":{"type":"boolean","description":"Explicitly advance Bird to its next allowed delivery channel. Earlier codes can remain valid. Unsupported for local SMS.","example":false}}},"example":{"challenge_id":"01JB0000000000000000000001","challenge_token":"YOUR_CHALLENGE_TOKEN","device_id":"install-demo-001"}}}}}},"\/me\/deletion\/status":{"post":{"operationId":"me_deletion_status","summary":"Status account deletion verification","description":"Read local worker state without provider HTTP or customer-side effects. Uses only the original business, app, device and caller capability. This cleanup operation remains available to the authenticated owner after normal app access is revoked.","tags":["Identity"],"responses":{"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"200":{"description":"Local state only. No code or session token is returned.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Pending until the worker records an outcome.","example":"pending"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26},"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"retry_after":{"type":"integer","description":"Seconds to wait before polling.","example":3},"outcome":{"type":"string","description":"sent, verified, invalid_code, expired, too_many_attempts or a safe failure reason.","example":"sent","nullable":true},"channel":{"type":"string","description":"Most recently observed delivery channel.","example":"whatsapp","nullable":true},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Current code expiry in the business time zone. Before remote send completion this is provisional; poll status for the provider expiry.","example":"2026-10-30T10:22:00+00:00"},"resend_after":{"type":"string","format":"date-time","nullable":true,"description":"Earliest resend time in the business time zone. Null while a remote send is pending, after the send limit, or when the challenge is no longer pending. Poll status after a queued send.","example":"2026-10-30T10:13:00+00:00"},"attempts_allowed":{"type":"integer","description":"Maximum number of code checks allowed for this challenge.","example":5},"attempts_left":{"type":"integer","minimum":0,"description":"Current checks remaining; zero after attempt exhaustion. The final wrong check returns too_many_attempts.","example":4},"error":{"type":"object","nullable":true,"description":"Public code-check error for a completed unsuccessful check. Status polling remains HTTP 200; null for pending work and successful sends\/checks.","properties":{"code":{"type":"string","description":"Same public code used by finalization: invalid_code, code_expired, too_many_attempts or challenge_not_found.","example":"invalid_code"},"message":{"type":"string","description":"Human-readable explanation of the check failure.","example":"That code is not right."},"attempts_left":{"type":"integer","minimum":0,"description":"Checks remaining, included for invalid_code.","example":4}}}}},"example":{"status":"pending","operation_id":"01JB0000000000000000000002","retry_after":3,"challenge_id":"01JB0000000000000000000001","expires_at":"2026-10-30T10:22:00+00:00","resend_after":null,"attempts_allowed":5,"attempts_left":5,"error":null}}}}},"parameters":[],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["challenge_id"],"properties":{"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"device_id":{"type":"string","description":"Original installation identifier for public login operations; authenticated operations use their bound installation.","example":"install-demo-001"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26}}},"example":{"challenge_id":"01JB0000000000000000000001","challenge_token":"YOUR_CHALLENGE_TOKEN","device_id":"install-demo-001"}}}}}},"\/me\/deletion\/resend":{"post":{"operationId":"me_deletion_resend","summary":"Resend account deletion verification","description":"Queue an explicit resend or next-channel request. Subject to local allowance, ownership and cooldown. Never changes provider. Uses only the original business, app, device and caller capability. This cleanup operation remains available to the authenticated owner after normal app access is revoked.","tags":["Identity"],"responses":{"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"$ref":"#\/components\/responses\/Error403"},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"426":{"$ref":"#\/components\/responses\/Error426"},"429":{"$ref":"#\/components\/responses\/Error429"},"202":{"description":"Remote verification is queued; no customer authority has been granted.","content":{"application\/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Pending until the worker records an outcome.","example":"pending"},"operation_id":{"type":"string","description":"Queued verification operation returned by the prior check or resend.","example":"01JB0000000000000000000002","minLength":26,"maxLength":26},"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"retry_after":{"type":"integer","description":"Seconds to wait before polling.","example":3},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Current code expiry in the business time zone. Before remote send completion this is provisional; poll status for the provider expiry.","example":"2026-10-30T10:22:00+00:00"},"resend_after":{"type":"string","format":"date-time","nullable":true,"description":"Earliest resend time in the business time zone. Null while a remote send is pending, after the send limit, or when the challenge is no longer pending. Poll status after a queued send.","example":"2026-10-30T10:13:00+00:00"},"attempts_allowed":{"type":"integer","description":"Maximum number of code checks allowed for this challenge.","example":5},"attempts_left":{"type":"integer","minimum":0,"description":"Current checks remaining; zero after attempt exhaustion. The final wrong check returns too_many_attempts.","example":4},"channel":{"type":"string","nullable":true,"description":"Most recently observed delivery channel; poll status for the newly queued send.","example":"sms"}}},"example":{"status":"pending","operation_id":"01JB0000000000000000000002","retry_after":3,"challenge_id":"01JB0000000000000000000001","expires_at":"2026-10-30T10:22:00+00:00","resend_after":null,"attempts_allowed":5,"attempts_left":5}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Required for a new Bird Verify check\/resend. Reuse for a transport retry with identical input; use a new key for a different code.","example":"verification-check-001","schema":{"type":"string","maxLength":100}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["challenge_id"],"properties":{"challenge_id":{"type":"string","description":"Opaque challenge identifier returned by the matching start operation.","example":"01JB0000000000000000000001","minLength":26,"maxLength":26},"challenge_token":{"type":"string","description":"Bird Verify caller capability returned at start. Keep in memory; never place it in a URL.","example":"YOUR_CHALLENGE_TOKEN","minLength":64,"maxLength":64},"device_id":{"type":"string","description":"Original installation identifier for public login operations; authenticated operations use their bound installation.","example":"install-demo-001"},"next_channel":{"type":"boolean","description":"Explicitly advance Bird to its next allowed delivery channel. Earlier codes can remain valid. Unsupported for local SMS.","example":false}}},"example":{"challenge_id":"01JB0000000000000000000001","challenge_token":"YOUR_CHALLENGE_TOKEN","device_id":"install-demo-001"}}}}}}},"components":{"securitySchemes":{"Bearer":{"type":"http","scheme":"bearer","description":"Token returned by POST \/auth\/verify for a customer installation. Continue sending X-App-Id on authenticated requests."},"AppId":{"type":"apiKey","in":"header","name":"X-App-Id","description":"Business app public identifier supplied by the platform team. Send X-App-Id on every customer API request, including sign-in. This identifies the business app and is not a customer authentication token."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","example":"invalid","description":"Stable application error code such as invalid or rate_limited. Use this value for application decisions, not message text."},"message":{"type":"string","description":"Human-readable message; do not parse its wording to decide application behavior."},"fields":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Validation messages keyed by request field name, including dotted paths for nested fields."},"retry_after":{"type":"integer","description":"Seconds to wait before retrying after a rate limit. Also honor the Retry-After response header."},"min_app_version":{"type":"string","description":"Minimum supported app version by platform, or the version required by an upgrade error."},"attempts_left":{"type":"integer","description":"Only on invalid_code: wrong codes still allowed before too_many_attempts."},"reason":{"type":"string","description":"Optional machine-readable account refusal reason. suspended is returned with account_unavailable after valid customer token authentication or successful sign-in code verification. Never infer suspension from an unauthenticated request.","example":"suspended"}},"required":["code","message"],"description":"Application error envelope. Use code for application behavior and message for display."}},"required":["error"],"description":"Standard application error response."},"Meta":{"type":"object","properties":{"current_page":{"type":"integer","example":1,"description":"One-based page number returned for this request."},"last_page":{"type":"integer","example":1,"description":"Last available page number for the current filters."},"per_page":{"type":"integer","example":20,"description":"Maximum number of records included per page, subject to the endpoint limit."},"total":{"type":"integer","example":1,"description":"Total matching records across all pages, not just the current page."}},"required":["current_page","last_page","per_page","total"],"description":"Pagination metadata for a bounded list response."},"Identifier":{"type":"object","properties":{"type":{"type":"string","enum":["phone","email"],"description":"Type discriminator for this object; use the documented enum when present."},"value":{"type":"string","description":"Identifier value, such as an email address or E.164 phone number."},"verified":{"type":"boolean","description":"Whether ownership of this identifier has been verified."}},"description":"Customer contact identifier and verification state."},"Customer":{"type":"object","properties":{"id":{"type":"string","minLength":26,"maxLength":26,"example":"01JB0000000000000000000001","description":"Customer public ULID within this business. Do not substitute a phone number or legacy database ID."},"name":{"type":"string","nullable":true,"example":"Alex Morgan","description":"Display name of the object represented here; do not use it as a stable identifier."},"locale":{"type":"string","nullable":true,"example":"en_GB","description":"Language\/region code such as en_GB; determines localized presentation."},"status":{"type":"string","enum":["active","provisional","suspended","merged","erased"],"description":"Customer lifecycle state, for example active or provisional. Suspended, merged or erased accounts cannot use normal authenticated operations. Suspended customers receive 403 account_unavailable with error.reason suspended instead of an authenticated profile."},"birthday":{"type":"string","nullable":true,"example":"1995-10-24","description":"Full date of birth as YYYY-MM-DD, for example 1995-10-24. Must be a valid calendar date, no later than today in the business timezone. Null clears it.","format":"date"},"push_consent":{"type":"boolean","description":"Customer consent for push marketing. Marketing delivery additionally requires marketing_consent and a reachable signed-in device."},"marketing_consent":{"type":"boolean","description":"Customer consent for marketing messages. Marketing push also requires push_consent; a push token alone is not consent."},"identifiers":{"type":"array","items":{"$ref":"#\/components\/schemas\/Identifier"},"description":"Customer contact identifiers and their ownership-verification state."},"identity_assurance":{"type":"string","enum":["verified_contact","anonymous","unverified_entry","demo"],"description":"Contact assurance, separate from lifecycle status. Anonymous accounts use browser sessions only; native sign-in remains contact-verified."},"recovery_available":{"type":"boolean","description":"At least one verified contact or an unexpired unused private recovery key exists. Wallet installation alone does not provide browser recovery."},"view_only_link_available":{"type":"boolean","description":"This active account has an active loyalty membership and can manage an optional view-only membership link through its authenticated browser session. Available to registered and anonymously enrolled members. The link never authorizes account access.","example":true}},"description":"Customer profile belonging to the resolved business."},"Membership":{"type":"object","properties":{"member_code":{"type":"string","example":"48213906574","description":"Private 11-digit membership code shown on the customer card or QR. Send as a string to preserve leading zeros; it is distinct from a reward code."},"balance":{"type":"integer","example":10,"description":"Accumulated points balance, not current-cycle progress. Render claim controls only from server claim decisions."},"spendable":{"type":"integer","example":10,"description":"Points available to spend, excluding holds. Claim decisions remain authoritative."},"lifetime":{"type":"integer","example":10,"description":"Total lifetime points earned by this membership."},"points_label":{"type":"string","description":"Business-specific display label for points, such as points or beans."},"cycle":{"$ref":"#\/components\/schemas\/Cycle"},"view_only_url":{"type":"string","format":"uri","nullable":true,"description":"Optional shareable view-only card URL showing membership code, points and progress. Anyone with the URL can view those fields. It cannot authenticate, recover an account, reveal vouchers or authorize claims. Null when no link is enabled.","example":"https:\/\/example.com\/experience\/customer\/cafe\/card\/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}},"description":"Customer loyalty membership, balances and optional milestone progress."},"Reward":{"type":"object","properties":{"id":{"type":"string","minLength":26,"maxLength":26,"example":"01JB0000000000000000000001","description":"Public ULID of the catalogue reward; pass it as reward when purchasing."},"name":{"type":"string","example":"Free coffee","description":"Display name of the object represented here; do not use it as a stable identifier."},"description":{"type":"string","nullable":true,"description":"Customer-facing description of this resource, when configured."},"type":{"type":"string","enum":["amount_voucher","free_item","percent_off"],"description":"Type discriminator for this object; use the documented enum when present."},"points_cost":{"type":"integer","nullable":true,"description":"Points required to purchase this catalogue reward."},"value_label":{"type":"string","description":"Formatted reward value for display, such as \u00a310.00, 15% off or Free item. Use numeric fields for calculations."},"value_minor":{"type":"integer","nullable":true,"example":1000,"description":"Fixed reward value in integer currency minor units; for example 1000 GBP means \u00a310.00."},"percent":{"type":"number","nullable":true,"description":"Percentage discount for a percentage reward, for example 15 means 15%."},"currency":{"type":"string","example":"GBP","description":"ISO 4217 currency code, for example GBP or AED. Monetary amounts are expressed in its minor units."},"claim":{"$ref":"#\/components\/schemas\/ClaimDecision"}},"description":"Catalogue reward definition. Its public ID is used when purchasing a reward."},"Grant":{"type":"object","properties":{"id":{"type":"string","minLength":26,"maxLength":26,"example":"01JB0000000000000000000001","description":"Public ULID of this issued reward grant. This differs from the catalogue reward ID and its redemption code."},"name":{"type":"string","example":"Free coffee","description":"Display name of the object represented here; do not use it as a stable identifier."},"type":{"type":"string","enum":["amount_voucher","free_item","percent_off"],"description":"Type discriminator for this object; use the documented enum when present."},"value_label":{"type":"string","description":"Formatted reward value for display, such as \u00a310.00, 15% off or Free item. Use numeric fields for calculations."},"value_minor":{"type":"integer","nullable":true,"example":1000,"description":"Fixed reward value in integer currency minor units; for example 1000 GBP means \u00a310.00."},"currency":{"type":"string","example":"GBP","description":"ISO 4217 currency code, for example GBP or AED. Monetary amounts are expressed in its minor units."},"code":{"type":"string","example":"DEMO7KQ2M","description":"Private redemption code. Absent on earned entitlements. Staff creation and lookup never return it; validation\/redemption require a code already supplied by the customer."},"status":{"type":"string","enum":["available","used","expired","revoked","earned"],"description":"Current lifecycle state of this object. Use the documented values and endpoint rules."},"used_at":{"type":"string","format":"date-time","nullable":true,"description":"ISO 8601 timestamp when the reward was used; null or omitted if it has not been used."},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Exclusive voucher cutoff in ISO 8601 with timezone offset. Redemption is refused at or after this instant. Null means no expiry. Use valid_through_date for date labels."},"claim":{"$ref":"#\/components\/schemas\/ClaimDecision"},"cycle":{"type":"integer","nullable":true,"description":"Card (cycle) a milestone reward came from; null for rewards bought with points."},"milestone":{"type":"integer","nullable":true,"description":"Milestone points on that card; null for rewards bought with points."},"issued_at":{"type":"string","format":"date-time","nullable":true,"description":"When the reward was issued; for the last milestone, when its card was completed."},"valid_through_date":{"type":"string","format":"date","nullable":true,"description":"Last inclusive valid calendar date in expiry_timezone. Null means no expiry."},"expiry_timezone":{"type":"string","nullable":true,"example":"Asia\/Dubai","description":"IANA business timezone captured when this reward was issued; unchanged by later country edits. Null means no expiry."}},"description":"An individual reward issued to a customer, including its private redemption code when permitted."},"StaffGrant":{"type":"object","properties":{"id":{"type":"string","minLength":26,"maxLength":26,"example":"01JB0000000000000000000001","description":"Public ULID of this issued reward grant. This differs from the catalogue reward ID and its redemption code."},"reward":{"$ref":"#\/components\/schemas\/Reward"},"code":{"type":"string","example":"DEMO7KQ2M","description":"Private redemption code. Absent on earned entitlements. Staff creation and lookup never return it; validation\/redemption require a code already supplied by the customer."},"status":{"type":"string","enum":["available","used","expired","revoked","earned"],"description":"Current lifecycle state of this object. Use the documented values and endpoint rules."},"used_at":{"type":"string","format":"date-time","nullable":true,"description":"ISO 8601 timestamp when the reward was used; null or omitted if it has not been used."},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Exclusive voucher cutoff in ISO 8601 with timezone offset. Redemption is refused at or after this instant. Null means no expiry. Use valid_through_date for date labels."},"bill_reference":{"type":"string","nullable":true,"example":"DEMO-BILL-001","description":"Receipt\/bill reference connecting the operation to the business transaction."},"claim":{"$ref":"#\/components\/schemas\/ClaimDecision"},"valid_through_date":{"type":"string","format":"date","nullable":true,"description":"Last inclusive valid calendar date in expiry_timezone. Null means no expiry."},"expiry_timezone":{"type":"string","nullable":true,"example":"Asia\/Dubai","description":"IANA business timezone captured when this reward was issued; unchanged by later country edits. Null means no expiry."}},"description":"Issued customer reward as visible to staff. Code visibility depends on how the customer was identified."},"Me":{"type":"object","properties":{"customer":{"$ref":"#\/components\/schemas\/Customer"},"membership":{"$ref":"#\/components\/schemas\/Membership","nullable":true},"rewards":{"type":"array","items":{"$ref":"#\/components\/schemas\/Grant"},"description":"Reward catalogue entries or customer reward grants, as defined by the item schema for this endpoint."},"google_wallet":{"type":"boolean","description":"Whether Google Wallet is available for this business\/customer."}},"description":"Signed-in customer profile with applicable loyalty and wallet information."},"Device":{"type":"object","properties":{"id":{"type":"string","maxLength":100,"example":"install-demo-001","description":"Stable installation ID, usually a UUID generated once by the app. Reuse the device_id sent to auth\/start; signing in with the same installation ID replaces its token."},"platform":{"type":"string","enum":["ios","android"],"description":"Native app platform. Supply ios or android.","example":"ios"},"name":{"type":"string","maxLength":100,"example":"Demo iPhone","description":"Optional human-readable installation label, such as Alex\u2019s iPhone."},"app_version":{"type":"string","maxLength":30,"description":"Installed app version such as 1.4.0. Required in practice when that platform has a minimum version policy.","example":"1.4.0"},"push_token":{"type":"string","maxLength":500,"nullable":true,"description":"FCM registration token for this installation. Send null to remove it and stop push delivery to this device."},"firebase_project_id":{"type":"string","maxLength":100,"nullable":true,"description":"Firebase project ID from this app build's Firebase options, sent with device.push_token. A token whose project the business has not set up for the app is not stored; sign-in still succeeds.","example":"cafe-app-prod"}},"required":["id","platform"],"description":"Installation metadata for the sign-in token. Signing in again with the same device ID replaces that installation token."},"Branch":{"type":"object","properties":{"id":{"type":"string","minLength":26,"maxLength":26,"example":"01JB0000000000000000000001","description":"Public branch ULID. Use it in GET \/branches\/{branch}; staff branch selection instead uses the staff integer branch ID."},"name":{"type":"string","example":"Demo High Street","description":"Display name of the object represented here; do not use it as a stable identifier."},"description":{"type":"string","nullable":true,"description":"Customer-facing description of this resource, when configured."},"photo_url":{"type":"string","nullable":true,"description":"Public URL of the branch image, or null when none is configured."},"address":{"type":"string","nullable":true,"description":"Branch street\/address text, or null when not configured."},"phone":{"type":"string","nullable":true,"example":"+447700900123","description":"Phone number. International E.164 format is recommended; customer inputs are normalized using the business country."},"latitude":{"type":"number","nullable":true,"description":"Branch latitude in decimal degrees, or null when not configured."},"longitude":{"type":"number","nullable":true,"description":"Branch longitude in decimal degrees, or null when not configured."},"map_url":{"type":"string","nullable":true,"description":"Map\/directions URL for the branch, or null when not configured."},"opening_hours":{"type":"array","items":{"type":"object","properties":{"day_of_week":{"type":"integer","description":"Day index: 0 is Sunday, 1 is Monday, through 6 for Saturday."},"closed":{"type":"boolean","description":"Whether this branch is closed for the whole day."},"open":{"type":"string","nullable":true,"description":"Local opening time in HH:MM format, or null on a closed day."},"close":{"type":"string","nullable":true,"description":"Local closing time in HH:MM format. A value earlier than open means closing after midnight."}}},"nullable":true,"description":"Weekly opening hours in the branch local time. Null means hours have not been configured."},"pickup_instructions":{"type":"string","nullable":true,"description":"Customer pickup instructions, or null when absent or ordering is unavailable."}},"description":"Customer-facing details of an active branch in the resolved business."},"StaffBranch":{"type":"object","properties":{"id":{"type":"integer","example":1,"description":"Legacy integer branch ID. Use this value as branch_id on staff operations; do not pass the public_id ULID."},"public_id":{"type":"string","minLength":26,"maxLength":26,"description":"Stable public ULID for this object. Keep it distinct from any legacy integer id."},"name":{"type":"string","example":"Demo High Street","description":"Display name of the object represented here; do not use it as a stable identifier."}},"description":"Authorized staff branch. Use the integer id for branch_id selection."},"Staff":{"type":"object","properties":{"id":{"type":"integer","example":1,"description":"Legacy integer ID of the authenticated staff account."},"name":{"type":"string","example":"Alex Morgan","description":"Display name of the object represented here; do not use it as a stable identifier."},"role":{"type":"string","enum":["business_staff","business_admin","super_admin"],"description":"Authenticated staff role. Available actions also depend on branch scope and enabled business features."},"business_id":{"type":"integer","description":"Legacy integer business ID associated with this account; it does not override the token tenant."},"branch_id":{"type":"integer","nullable":true,"description":"Legacy integer branch ID from the authorized branch list, not its public ULID. Managers supply a branch when needed; assigned staff may only use their own branch."},"permissions":{"type":"array","items":{"type":"string"},"description":"Currently available operation identifiers for the authenticated staff\/branch context. They are UI hints; the server rechecks authorization."},"branches":{"type":"array","items":{"$ref":"#\/components\/schemas\/StaffBranch"},"description":"Bounded legacy list of authorized branches, limited to 100. Use the paginated GET \/branches endpoint for discovery."}},"description":"Authenticated staff identity, assignment and bounded legacy branch list. Use GET \/branches for complete discovery."},"LookupCustomer":{"type":"object","properties":{"member_code":{"type":"string","pattern":"^\\d{11}$","example":"48213906574","description":"Customer membership QR\/card value: exactly 11 digits, sent as a string. Supply exactly one of member_code, phone or email. A member-code lookup may return private reward codes."},"phone":{"type":"string","example":"+447700900123","description":"Customer phone for an earning-only lookup. E.164 is recommended; local numbers use the business country. Supply exactly one lookup identifier."},"email":{"type":"string","format":"email","example":"customer@example.com","description":"Customer email for an earning-only lookup. Supply exactly one lookup identifier; email lookup never authorizes redemption."}},"description":"Identify a customer with exactly one of member_code, phone or email. Phone\/email support earning, not redemption authorization."},"Calculation":{"type":"object","properties":{"points":{"type":"integer","example":2,"description":"Points earned, moved or accumulated in this context; these are loyalty points, not a currency amount."},"explanation":{"type":"array","items":{"type":"string"},"description":"Human-readable calculation notes explaining how the published loyalty rules produced the points result."}},"description":"Calculated points and human-readable rule notes."},"Lookup":{"type":"object","properties":{"identified_by":{"type":"string","description":"Identifier used to resolve this customer: member code, phone or email. This affects which private codes may be returned."},"customer":{"type":"object","properties":{"id":{"type":"string","minLength":26,"maxLength":26,"example":"01JB0000000000000000000001","description":"Identifier of this object. Preserve the documented type and use the ID rather than a display name."},"name":{"type":"string","nullable":true,"example":"Alex Morgan","description":"Display name of the object represented here; do not use it as a stable identifier."},"status":{"type":"string","description":"Current lifecycle state of this object. Use the documented values and endpoint rules."},"can_redeem":{"type":"boolean","description":"Whether the customer account is active for redemption. Individual rewards still have their own status and eligibility checks."}},"description":"Customer identity associated with this operation."},"membership":{"$ref":"#\/components\/schemas\/Membership","nullable":true},"rewards":{"type":"array","items":{"$ref":"#\/components\/schemas\/StaffGrant"},"description":"Reward catalogue entries or customer reward grants, as defined by the item schema for this endpoint."}},"description":"Customer lookup result. Phone\/email lookup does not reveal membership or redemption codes."},"Award":{"type":"object","properties":{"customer":{"type":"object","properties":{"id":{"type":"string","minLength":26,"maxLength":26,"example":"01JB0000000000000000000001","description":"Identifier of this object. Preserve the documented type and use the ID rather than a display name."},"status":{"type":"string","description":"Current lifecycle state of this object. Use the documented values and endpoint rules."},"new":{"type":"boolean","description":"Whether this operation created a provisional customer rather than finding an existing one."}},"description":"Customer identity associated with this operation."},"transaction_id":{"type":"string","minLength":26,"maxLength":26,"nullable":true,"example":"01JB0000000000000000000001","description":"Public transaction\/audit ULID returned by a write. Save it for reconciliation and supported reversal workflows; it may be null when no ledger entry was created."},"points":{"type":"integer","example":2,"description":"Points earned, moved or accumulated in this context; these are loyalty points, not a currency amount."},"balance":{"type":"integer","nullable":true,"example":10,"description":"Current points balance. Use spendable, when provided, to decide how many points can be redeemed."},"replayed":{"type":"boolean","description":"Whether the underlying operation reports a previously applied result rather than a new mutation. Always retain and reuse the original idempotency key."},"calculation":{"$ref":"#\/components\/schemas\/Calculation"},"rewards_unlocked":{"type":"array","items":{"$ref":"#\/components\/schemas\/StaffGrant"},"description":"Reward grants unlocked by this purchase. Codes are not exposed merely by a phone\/email lookup."}},"description":"Purchase\/award result with points, balance, calculation and newly unlocked rewards."},"History":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["earn","redeem","adjust","reversal","expiry","refund","claim_in","claim_out"],"description":"Type discriminator for this object; use the documented enum when present."},"points":{"type":"integer","example":2,"description":"Signed loyalty points movement for this ledger entry; positive earns and negative debits are expressed in points, not currency."},"balance_after":{"type":"integer","description":"Points balance immediately after this ledger entry."},"status":{"type":"string","description":"Ledger posting state: posted points have been applied; held points are not yet spendable."},"branch":{"type":"string","nullable":true,"description":"Branch details or branch display name associated with this result."},"bill_reference":{"type":"string","nullable":true,"example":"DEMO-BILL-001","description":"Receipt\/bill reference connecting the operation to the business transaction."},"reason":{"type":"string","nullable":true,"description":"Human-readable reason for the correction, retained in the audit trail."},"notes":{"type":"array","items":{"type":"string"},"description":"Human-readable calculation notes associated with this ledger entry."},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 creation timestamp of the record."}}},"description":"Loyalty ledger entries in this page, ordered newest first."},"meta":{"$ref":"#\/components\/schemas\/Meta"}},"description":"Paginated customer loyalty ledger history."},"ClaimDecision":{"type":"object","description":"Server-calculated snapshot; mutations always recheck current eligibility.","properties":{"eligible":{"type":"boolean","description":"Whether this actor may claim this reward now.","example":true},"reason_code":{"type":"string","nullable":true,"description":"Stable refusal reason, or null when eligible.","example":null},"message":{"type":"string","nullable":true,"description":"Human-readable refusal message.","example":null},"points_required":{"type":"integer","nullable":true,"description":"Points spent at claim creation; zero for an earned entitlement.","example":20},"available_actions":{"type":"array","description":"Actions the client may present. Discovery is not authorization.","items":{"type":"string","enum":["claim"]},"example":["claim"]}}},"Cycle":{"type":"object","nullable":true,"required":["number","points","target","points_to_next","next_reward","milestones","reset_after_last","carry_over","completed"],"description":"Published ladder progress, separate from accumulated balance. Reads never issue rewards. Null without an active supported ladder.","properties":{"number":{"type":"integer","description":"Current cycle number."},"points":{"type":"integer","description":"Current-cycle points; distinct from accumulated balance."},"target":{"type":"integer","nullable":true,"description":"Published final milestone threshold; null without a threshold."},"points_to_next":{"type":"integer","nullable":true,"description":"Points remaining to the next offered higher threshold; null when none."},"next_reward":{"type":"object","nullable":true,"required":["id","name","type","value_label","icon","threshold"],"properties":{"id":{"type":"string","description":"Public object ID; event IDs include their source prefix."},"name":{"type":"string","description":"Customer-facing display name."},"threshold":{"type":"integer","description":"Points threshold of the next offered reward."},"type":{"type":"string","enum":["free_item","amount_voucher","percent_off"],"description":"Reward kind, for the reward stop's badge."},"value_label":{"type":"string","nullable":true,"description":"Short badge value such as \u00a310 or 10%; null for free items."},"icon":{"type":"string","enum":["gift","coffee","drink","food","dessert","money","percent","ticket","star","bag"],"description":"Picture for the reward: the business's pick, else money (amount voucher), percent (percent off) or gift (free item). Treat unknown values as gift."}},"description":"Next offered reward above current-cycle points, or null."},"reset_after_last":{"type":"boolean","description":"Whether earning through the last threshold starts the next cycle."},"carry_over":{"type":"boolean","description":"Whether excess earning carries into the next cycle."},"milestones":{"type":"array","maxItems":10,"items":{"type":"object","required":["points","reward","status","entitlement_id","reached","steps"],"properties":{"points":{"type":"integer","description":"Current-cycle points; distinct from accumulated balance."},"reward":{"type":"object","nullable":true,"required":["id","name","type","value_label","icon"],"properties":{"id":{"type":"string","description":"Public object ID; event IDs include their source prefix."},"name":{"type":"string","description":"Customer-facing display name."},"type":{"type":"string","enum":["free_item","amount_voucher","percent_off"],"description":"Reward kind, for the reward stop's badge."},"value_label":{"type":"string","nullable":true,"description":"Short badge value such as \u00a310 or 10%; null for free items."},"icon":{"type":"string","enum":["gift","coffee","drink","food","dessert","money","percent","ticket","star","bag"],"description":"Picture for the reward: the business's pick, else money (amount voucher), percent (percent off) or gift (free item). Treat unknown values as gift."}},"description":"Related reward identity and display name, or null for ledger-only activity."},"status":{"type":"string","enum":["locked","pending","unavailable","earned","available","used","expired","revoked"],"description":"Milestone\/grant state, or ledger posting status in activity. pending means reached but unissued, and never authorizes a claim. A reward a refund took back reports locked (or pending once reached), as it is issued again; revoked means revoked by staff."},"entitlement_id":{"type":"string","nullable":true,"description":"Owned grant public ID for this cycle\/milestone, or null."},"reached":{"type":"boolean","description":"Current-cycle points have reached this milestone."},"steps":{"type":"array","maxItems":9,"description":"Stops a card shows before this milestone's reward, split by the server (one per point up to 10, else 5-10 even stops, else 5 rounded). Draw as given.","items":{"type":"object","required":["points","value","reached"],"properties":{"points":{"type":"integer","description":"Cumulative current-cycle threshold of this stop."},"value":{"type":"integer","description":"Points this stop is worth (from the previous stop)."},"reached":{"type":"boolean","description":"Current-cycle points have reached this stop."}}}}}},"description":"Published ladder milestones, at most ten. State does not authorize a claim."},"completed":{"type":"object","nullable":true,"required":["number","target","cards","completed_at","milestones"],"description":"The card the member's latest point-earning purchase filled (its last-milestone reward came from that purchase, is still the member's own and was not refunded). Null otherwise, including once points are earned again, once no reward on it is still waiting (all used, expired or withdrawn), for ladders that do not start a new card, and after a refund takes that reward back. Display only: the cycle has already moved on and its rewards are issued.","properties":{"number":{"type":"integer","description":"Cycle number of the filled card."},"target":{"type":"integer","description":"Points on a full card."},"cards":{"type":"integer","description":"Cards that purchase filled (more than one for a very large purchase)."},"completed_at":{"type":"string","format":"date-time","nullable":true,"description":"When its last reward was issued."},"milestones":{"type":"array","maxItems":10,"items":{"type":"object","required":["points","reward","status","entitlement_id","reached","steps"],"properties":{"points":{"type":"integer","description":"Current-cycle points; distinct from accumulated balance."},"reward":{"type":"object","nullable":true,"required":["id","name","type","value_label","icon"],"properties":{"id":{"type":"string","description":"Public object ID; event IDs include their source prefix."},"name":{"type":"string","description":"Customer-facing display name."},"type":{"type":"string","enum":["free_item","amount_voucher","percent_off"],"description":"Reward kind, for the reward stop's badge."},"value_label":{"type":"string","nullable":true,"description":"Short badge value such as \u00a310 or 10%; null for free items."},"icon":{"type":"string","enum":["gift","coffee","drink","food","dessert","money","percent","ticket","star","bag"],"description":"Picture for the reward: the business's pick, else money (amount voucher), percent (percent off) or gift (free item). Treat unknown values as gift."}},"description":"Related reward identity and display name, or null for ledger-only activity."},"status":{"type":"string","enum":["locked","pending","unavailable","earned","available","used","expired","revoked"],"description":"Milestone\/grant state, or ledger posting status in activity. pending means reached but unissued, and never authorizes a claim. A reward a refund took back reports locked (or pending once reached), as it is issued again; revoked means revoked by staff."},"entitlement_id":{"type":"string","nullable":true,"description":"Owned grant public ID for this cycle\/milestone, or null."},"reached":{"type":"boolean","description":"Current-cycle points have reached this milestone."},"steps":{"type":"array","maxItems":9,"description":"Stops a card shows before this milestone's reward, split by the server (one per point up to 10, else 5-10 even stops, else 5 rounded). Draw as given.","items":{"type":"object","required":["points","value","reached"],"properties":{"points":{"type":"integer","description":"Cumulative current-cycle threshold of this stop."},"value":{"type":"integer","description":"Points this stop is worth (from the previous stop)."},"reached":{"type":"boolean","description":"Current-cycle points have reached this stop."}}}}}},"description":"Published ladder milestones, at most ten. State does not authorize a claim."}}}}},"Activity":{"type":"object","required":["entries","meta"],"properties":{"entries":{"type":"array","items":{"type":"object","required":["id","type","created_at","points","balance_after","reward","branch","status","bill_reference","reverses_event_id"],"properties":{"id":{"type":"string","description":"Stable source-prefixed public event ID."},"type":{"type":"string","enum":["earn","redeem","adjust","reversal","expiry","refund","claim_in","claim_out","reward_used","reward_use_voided"],"description":"Explicit event type. redeem is points spent at catalogue creation; reward_used is voucher use."},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 event occurrence timestamp."},"points":{"type":"integer","nullable":true,"description":"Signed ledger point amount; null for voucher use\/void, which do not spend points."},"balance_after":{"type":"integer","nullable":true,"description":"Ledger balance after this entry, or null for voucher use\/void."},"reward":{"type":"object","nullable":true,"required":["id","name"],"properties":{"id":{"type":"string","description":"Public object ID; event IDs include their source prefix."},"name":{"type":"string","description":"Customer-facing display name."}},"description":"Related reward identity and display name, or null for ledger-only activity."},"branch":{"type":"object","nullable":true,"properties":{"id":{"type":"integer","description":"Public object ID; event IDs include their source prefix."},"name":{"type":"string","description":"Customer-facing display name."}},"description":"Branch identity and name, or null when absent."},"status":{"type":"string","nullable":true,"description":"Milestone\/grant state, or ledger posting status in activity. pending means reached but unissued, and never authorizes a claim."},"bill_reference":{"type":"string","nullable":true,"description":"Bill reference supplied for the operation, or null."},"reverses_event_id":{"type":"string","nullable":true,"description":"Source-prefixed use event ID being voided, or null."}}},"description":"Activity events ordered by time, source and internal ID, newest first."},"meta":{"type":"object","required":["per_page","has_more","next_cursor"],"properties":{"per_page":{"type":"integer","description":"Requested bounded page size."},"has_more":{"type":"boolean","description":"Whether another page is available."},"next_cursor":{"type":"string","nullable":true,"description":"Opaque cursor for the next page, or null; retain the selected filter."}},"description":"Cursor pagination metadata."}}},"CountryInputProfile":{"type":"object","description":"Country-specific presentation hints resolved from the business country. Apply these labels and placeholders without treating phone hints as validation masks. International phone numbers remain allowed.","required":["country_code","country_name","dialing_code","address_format","fields"],"properties":{"country_code":{"type":"string","nullable":true,"description":"ISO 3166-1 alpha-2 code of the business country, or null when not configured."},"country_name":{"type":"string","nullable":true,"description":"Display name of the business country, or null when not configured."},"dialing_code":{"type":"string","nullable":true,"description":"Default international dialing prefix used to interpret local phone input; international numbers remain allowed."},"address_format":{"type":"string","nullable":true,"description":"Configured country address-format hint, or null when not configured."},"fields":{"type":"object","required":["phone","city","area","province","postal_code","address"],"properties":{"phone":{"type":"object","required":["label","placeholder"],"properties":{"label":{"type":"string","description":"Localized display label for this input."},"placeholder":{"type":"string","description":"Example input hint; not a validation mask or a required value."},"suggestions":{"type":"array","items":{"type":"string"},"description":"Optional non-exhaustive suggestions; free text remains valid."}},"description":"Presentation hints for the phone field; these do not impose additional validation rules."},"city":{"type":"object","required":["label","placeholder"],"properties":{"label":{"type":"string","description":"Localized display label for this input."},"placeholder":{"type":"string","description":"Example input hint; not a validation mask or a required value."},"suggestions":{"type":"array","items":{"type":"string"},"description":"Optional non-exhaustive suggestions; free text remains valid."}},"description":"Presentation hints for the city field; these do not impose additional validation rules."},"area":{"type":"object","required":["label","placeholder"],"properties":{"label":{"type":"string","description":"Localized display label for this input."},"placeholder":{"type":"string","description":"Example input hint; not a validation mask or a required value."},"suggestions":{"type":"array","items":{"type":"string"},"description":"Optional non-exhaustive suggestions; free text remains valid."}},"description":"Presentation hints for the area field; these do not impose additional validation rules."},"province":{"type":"object","required":["label","placeholder"],"properties":{"label":{"type":"string","description":"Localized display label for this input."},"placeholder":{"type":"string","description":"Example input hint; not a validation mask or a required value."},"suggestions":{"type":"array","items":{"type":"string"},"description":"Optional non-exhaustive suggestions; free text remains valid."}},"description":"Presentation hints for the province field; these do not impose additional validation rules."},"postal_code":{"type":"object","required":["label","placeholder"],"properties":{"label":{"type":"string","description":"Localized display label for this input."},"placeholder":{"type":"string","description":"Example input hint; not a validation mask or a required value."},"suggestions":{"type":"array","items":{"type":"string"},"description":"Optional non-exhaustive suggestions; free text remains valid."}},"description":"Presentation hints for the postal code field; these do not impose additional validation rules."},"address":{"type":"object","required":["label","placeholder"],"properties":{"label":{"type":"string","description":"Localized display label for this input."},"placeholder":{"type":"string","description":"Example input hint; not a validation mask or a required value."},"suggestions":{"type":"array","items":{"type":"string"},"description":"Optional non-exhaustive suggestions; free text remains valid."}},"description":"Presentation hints for the address field; these do not impose additional validation rules."}},"description":"Localized field labels, example placeholders and optional non-exhaustive suggestions."}}},"PhoneOtpCountries":{"type":"object","description":"Countries this business can send phone verification codes to. Super admins switch them on per business from a reference list of every country; the business's own country is on by default. Build the phone country-code picker from these only; the server refuses other numbers with 422 country_not_allowed. An empty list means phone codes are unavailable.","required":["default_country","countries"],"properties":{"default_country":{"type":"string","nullable":true,"description":"ISO 3166-1 alpha-2 code to preselect: the business country when allowed, else the first listed; null when the list is empty."},"countries":{"type":"array","description":"Business country first, then by name.","items":{"type":"object","required":["code","name","dialing_code","flag"],"properties":{"code":{"type":"string","description":"ISO 3166-1 alpha-2 code.","example":"GB"},"name":{"type":"string","description":"Country name in English.","example":"United Kingdom"},"dialing_code":{"type":"string","description":"International dialing prefix with a leading +.","example":"+44"},"flag":{"type":"string","description":"Flag emoji.","example":"\ud83c\uddec\ud83c\udde7"}}}}}}},"responses":{"Error400":{"description":"Missing idempotency key \u2014 Supply a stable key for the intended operation, then retry.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"idempotency_key_required","message":"An Idempotency-Key header is required."}}}}},"Error401":{"description":"Authentication required \u2014 Sign in again when the token is expired or revoked. Check X-App-Id for customer requests.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"unauthenticated","message":"Authentication required."}}}}},"Error403":{"description":"Customer access denied. For an otherwise valid customer token or a successfully verified sign-in code, account_unavailable with reason suspended means the account is on hold. Show the suspension screen and stop automatic retries. Invalid\/expired\/revoked\/wrong-business tokens remain 401; auth\/start does not reveal account state. Other 403 codes retain their feature\/access meanings.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"examples":{"access_denied":{"summary":"Feature or API access is unavailable","value":{"error":{"code":"api_access_unavailable","message":"API access is unavailable."}}},"account_suspended":{"summary":"Customer account is suspended","value":{"error":{"code":"account_unavailable","reason":"suspended","message":"This account is suspended. Please contact the business."}}}}}}},"Error404":{"description":"Not found \u2014 Check the public identifier and business context. Resources belonging to another tenant are not accessible.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"not_found","message":"Not found."}}}}},"Error409":{"description":"Conflict \u2014 Reuse a key only for an identical operation. For voucher_already_used, refresh reward status instead of redeeming again.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"idempotency_conflict","message":"This key was used with different request details."}}}}},"Error422":{"description":"Validation or domain refusal \u2014 Correct the fields or handle the domain refusal identified by error.code. Do not retry an unchanged invalid request.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"invalid","message":"Some fields are invalid.","fields":{"field_name":["A validation message."]}}}}}},"Error426":{"description":"App upgrade required \u2014 Use the supported minimum app version reported by configuration.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"upgrade_required","message":"Please update the app."}}}}},"Error429":{"description":"Rate limited; Retry-After header \u2014 Wait for the Retry-After header before retrying; retain the same operation key for a mutation.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests; slow down.","retry_after":60}}}},"headers":{"Retry-After":{"description":"Seconds until a retry is allowed","schema":{"type":"integer"}}}}}},"tags":[{"name":"Configuration"},{"name":"Branches"},{"name":"Authentication"},{"name":"Devices"},{"name":"Account"},{"name":"Rewards"},{"name":"Wallet & channels"},{"name":"Identity"}]}