{"openapi":"3.0.3","info":{"title":"Staff App API","version":"1.6.0","description":"First-party staff 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. Android, iOS and browser Store App access uses Internal App API Access independently of branded customer apps. Neither X-App-Id, branded app provisioning nor an External POS API key is required. Re-fetch \/config after branch changes and authorization errors; cached capabilities never authorize work. Staff role labels are not a complete authorization contract: legacy\/shadow businesses retain legacy rules; enforced businesses use current branch-scoped grants and denies. Use \/config.capabilities or staff.permissions for operation aliases, refresh after authorization errors, and honor endpoint-specific permissions even on idempotent replays. Unavailable authorization state returns 503 authorization_unavailable with Retry-After: 3 rather than falling back to legacy access."},"servers":[{"url":"\/api\/staff\/v1"}],"security":[{"Bearer":[]}],"paths":{"\/login":{"post":{"operationId":"staffLogin","summary":"Personal username\/password login; no X-App-Id","responses":{"201":{"description":"Success","content":{"application\/json":{"schema":{"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."},"staff":{"$ref":"#\/components\/schemas\/Staff"}}}}}},"401":{"description":"Unknown username or incorrect password. Disabled status is disclosed only after a correct password.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"examples":{"invalid":{"value":{"error":{"code":"invalid_credentials","message":"The username or password is incorrect."}}}}}}},"403":{"description":"Correct credentials for disabled staff return staff_disabled. Internal API entitlement failures return api_access_unavailable.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"examples":{"disabled":{"value":{"error":{"code":"staff_disabled","message":"This staff account is switched off."}}},"access":{"value":{"error":{"code":"api_access_unavailable","message":"Internal app API access is not enabled."}}}}}}},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"$ref":"#\/components\/responses\/Error422"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"security":[],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"username":{"type":"string","maxLength":100,"description":"Personal staff username issued by the business administrator; do not substitute a customer phone or app ID.","example":"demo.staff"},"password":{"type":"string","format":"password","maxLength":255,"description":"Password for the staff account. Send over HTTPS and never save it in shared examples or logs.","example":"YOUR_STAFF_PASSWORD"},"device_name":{"type":"string","maxLength":100,"description":"Set by the app, not typed by the staff member. Names this app installation's sign-in session, up to 100 characters. On first sign-in, build it from the device model the operating system reports plus a short random suffix, for example iPad - 3f9c2a71 (iOS UIDevice.current.model) or Samsung SM-S921B - a7164466 (Android Build.MANUFACTURER and Build.MODEL). Save it on the device and send the same value on every later sign-in, including after logout. Signing in again with the same staff account and device_name ends that installation's previous session; other devices stay signed in. Do not create a new name for each sign-in or send a shared value such as Android or iPhone. No hardware ID or IMEI is needed.","example":"iPad - 3f9c2a71"}},"required":["username","password","device_name"]},"example":{"username":"demo.staff","password":"YOUR_STAFF_PASSWORD","device_name":"iPad - 3f9c2a71"}}}},"tags":["Authentication"],"description":"Sign in with the staff member\u2019s personal username and password. No X-App-Id is required. New tokens expire after 12 hours; login is limited to five attempts per username per minute. Operation aliases in staff.permissions and \/config.capabilities reflect current branch scope, business access and authorization mode. Do not infer allowed operations from role alone; enforced mode supports grants to staff and denies to managers. Refresh after a branch change or 403. Authorization lookup failures can return 503 authorization_unavailable; honor Retry-After. device_name is generated and stored by the app, not entered by the staff member (see the field). Signing in ends the earlier session for the same staff account and device_name; other devices stay signed in. Authenticated rate limits are shared across all tokens of a staff principal (120\/minute)."}},"\/me":{"get":{"operationId":"getStaffMe","summary":"Profile, permissions, branch and device","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"staff":{"$ref":"#\/components\/schemas\/Staff"},"branch":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of this object. Preserve the documented type and use the ID rather than a display name."},"name":{"type":"string","description":"Display name of the object represented here; do not use it as a stable identifier."}},"nullable":true,"description":"Branch details or branch display name associated with this result."},"device":{"type":"string","description":"Installation details used to issue a device token and register optional push delivery."}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"parameters":[{"name":"branch_id","in":"query","required":false,"schema":{"type":"integer"},"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. Omit if no selection is needed; a manager may receive no active branch until one is selected.","example":12}],"tags":["Authentication"],"description":"Read the signed-in staff member, assigned branch, device and permissions. Use the returned permissions to control available actions; the server enforces them on every operation. Managers without a branch receive branch: null and can still fetch config, discover branches and logout. Operation aliases in staff.permissions and \/config.capabilities reflect current branch scope, business access and authorization mode. Do not infer allowed operations from role alone; enforced mode supports grants to staff and denies to managers. Refresh after a branch change or 403. Authorization lookup failures can return 503 authorization_unavailable; honor Retry-After."}},"\/logout":{"post":{"operationId":"staffLogout","summary":"Revoke this staff 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"},"429":{"$ref":"#\/components\/responses\/Error429"}},"tags":["Authentication"],"description":"Revoke the current staff token and remove it from secure device storage."}},"\/customers\/lookup":{"post":{"operationId":"lookupCustomer","summary":"Exactly one identifier; phone\/email never expose redemption codes","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Lookup"}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"member_code":{"type":"string","pattern":"^\\d{11}$","description":"Customer membership QR\/card value: exactly 11 digits, sent as a string. Supply exactly one of member_code, phone or email. Store lookup never returns reward codes, even after a membership scan.","example":"48213906574"},"phone":{"type":"string","description":"Customer phone for an earning-only lookup. E.164 is recommended; local numbers use the business country. Supply exactly one lookup identifier.","example":"+447700900123","maxLength":30},"email":{"type":"string","format":"email","description":"Customer email for an earning-only lookup. Supply exactly one lookup identifier; email lookup never authorizes redemption.","example":"customer@example.com","maxLength":255},"branch_id":{"type":"integer","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.","example":12}},"description":"Supply exactly one of member_code, phone or email. Phone\/email lookups never expose membership or redemption codes."},"example":{"member_code":"48213906574"}}}},"tags":["Customers"],"description":"Identify a customer using exactly one supported identifier. Phone\/email lookups are for earning and never expose member or redemption codes. A membership scan may initiate voucher creation; redemption requires the separate customer-supplied reward code. In enforced mode, the current branch requires loyalty.members.lookup. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization."}},"\/customers":{"post":{"operationId":"createTillCustomer","summary":"Find or create a claimable provisional phone account","responses":{"201":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Lookup"}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Lookup"}}}},"503":{"$ref":"#\/components\/responses\/Error503"}},"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":{"phone":{"type":"string","description":"Phone number for the till customer. E.164 is recommended; local format uses the business country. Finds an existing account or creates a provisional one; redemption requires verification.","example":"+447700900123","maxLength":30},"branch_id":{"type":"integer","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.","example":12}},"required":["phone"]},"example":{"phone":"+447700900123"}}}},"tags":["Customers"],"description":"Find or create a provisional customer by phone. Provisional accounts can earn rewards but must verify their identity before redeeming. In enforced mode, the current branch requires customers.create. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization."}},"\/purchases\/preview":{"post":{"operationId":"previewPurchase","summary":"Calculate points without posting","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"new_customer":{"type":"boolean","description":"Whether the preview found no existing customer. Preview does not create a customer or write points."},"calculation":{"$ref":"#\/components\/schemas\/Calculation"}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"customer":{"allOf":[{"$ref":"#\/components\/schemas\/LookupCustomer"}],"description":"Lookup object containing exactly one member_code, phone or email. Phone can create a provisional till customer on a purchase; member_code\/email must resolve an existing customer."},"amount_paid_minor":{"type":"integer","minimum":0,"description":"Bill amount paid in integer currency minor units before subtracting excluded_minor. For example 2550 GBP means \u00a325.50. Required only when GET \/config returns loyalty.bill_amount_required: true (the default). Otherwise it may be omitted or null and is evaluated as zero spend. A supplied value must satisfy this field's integer\/minimum constraints; staff limits still apply.","example":5000,"nullable":true},"excluded_minor":{"type":"integer","minimum":0,"description":"Portion of amount_paid_minor excluded from earning, in the same currency minor units. Omit for zero.","example":0},"branch_id":{"type":"integer","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.","example":12},"bill_reference":{"type":"string","maxLength":100,"description":"Optional receipt\/bill reference used to build the preview context. Preview does not record a purchase or reserve points\/rewards.","nullable":true},"occurred_at":{"type":"string","format":"date-time","description":"Purchase timestamp in ISO 8601 with timezone. If supplied it must be within the previous 24 hours and not in the future; omitted uses server time. Use server-current time or omit this field in executable examples. Client timestamps do not move the server processing time used for point holds, point expiry or current-day earning limits.","example":"2026-10-25T10:00:00Z"},"channel":{"type":"string","enum":["pos","app"],"description":"Compatibility input: pos and app are accepted, but the server always records these staff\/POS purchase operations with source pos. Sending app cannot select app-only earning rules.","example":"pos","deprecated":true},"lines":{"type":"array","maxItems":200,"description":"Optional bill lines for item rules (points per item bought, bill contains items). Each line needs a sku or a category. A refund by amount reduces quantities in proportion to what is still paid, rounded down. Repeating an Idempotency-Key with different lines is a conflict.","items":{"type":"object","required":["quantity"],"properties":{"sku":{"type":"string","maxLength":64,"description":"Item code as the POS knows it; required when category is absent.","example":"LATTE"},"category":{"type":"string","maxLength":64,"description":"Item category as the POS knows it; required when sku is absent.","example":"coffee"},"quantity":{"type":"integer","minimum":1,"maximum":10000,"description":"Units of this item on the bill.","example":2},"amount_minor":{"type":"integer","minimum":0,"description":"What this line cost, in currency minor units; informational.","example":900}}}}},"required":["customer"]},"example":{"customer":{"member_code":"48213906574"},"amount_paid_minor":5000,"excluded_minor":0}}}},"tags":["Earning"],"description":"Calculate the current policy\u2019s points without posting a transaction. Use amount_paid_minor after discounts and excluded_minor for ineligible spend. A preview does not reserve or award points. In enforced mode, the current branch requires loyalty.points.preview. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization. The recorded source is always pos. Point holds and expiry start from server processing time, not a supplied backdated occurred_at. The amount requirement follows loyalty.bill_amount_required from GET \/config; an optional omitted amount counts as zero spend. Bill reference remains optional regardless of the recording policy."}},"\/purchases":{"post":{"operationId":"recordPurchase","summary":"Post server-calculated points","responses":{"201":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Award"}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Award"}}}},"503":{"$ref":"#\/components\/responses\/Error503"}},"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":{"customer":{"allOf":[{"$ref":"#\/components\/schemas\/LookupCustomer"}],"description":"Lookup object containing exactly one member_code, phone or email. Phone can create a provisional till customer on a purchase; member_code\/email must resolve an existing customer."},"amount_paid_minor":{"type":"integer","minimum":0,"description":"Bill amount paid in integer currency minor units before subtracting excluded_minor. For example 2550 GBP means \u00a325.50. Required only when GET \/config returns loyalty.bill_amount_required: true (the default). Otherwise it may be omitted or null and is evaluated as zero spend. A supplied value must satisfy this field's integer\/minimum constraints; staff limits still apply.","example":5000,"nullable":true},"excluded_minor":{"type":"integer","minimum":0,"description":"Portion of amount_paid_minor excluded from earning, in the same currency minor units. Omit for zero.","example":0},"branch_id":{"type":"integer","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.","example":12},"bill_reference":{"type":"string","maxLength":100,"description":"Stable receipt\/bill reference for the earning purchase in this branch, up to 100 characters. Keep the same value on retries and later refunds; it is separate from Idempotency-Key. Required only when GET \/config returns loyalty.bill_reference_required: true (the default); otherwise omission or null is accepted. A provided value must be a string of at most 100 characters. Without a reference, reference-based refunds and duplicate-bill detection are unavailable; idempotency remains required.","example":"DEMO-BILL-001","nullable":true},"occurred_at":{"type":"string","format":"date-time","description":"Purchase timestamp in ISO 8601 with timezone. If supplied it must be within the previous 24 hours and not in the future; omitted uses server time. Use server-current time or omit this field in executable examples. Client timestamps do not move the server processing time used for point holds, point expiry or current-day earning limits.","example":"2026-10-25T10:00:00Z"},"channel":{"type":"string","enum":["pos","app"],"description":"Compatibility input: pos and app are accepted, but the server always records these staff\/POS purchase operations with source pos. Sending app cannot select app-only earning rules.","example":"pos","deprecated":true},"lines":{"type":"array","maxItems":200,"description":"Optional bill lines for item rules (points per item bought, bill contains items). Each line needs a sku or a category. A refund by amount reduces quantities in proportion to what is still paid, rounded down. Repeating an Idempotency-Key with different lines is a conflict.","items":{"type":"object","required":["quantity"],"properties":{"sku":{"type":"string","maxLength":64,"description":"Item code as the POS knows it; required when category is absent.","example":"LATTE"},"category":{"type":"string","maxLength":64,"description":"Item category as the POS knows it; required when sku is absent.","example":"coffee"},"quantity":{"type":"integer","minimum":1,"maximum":10000,"description":"Units of this item on the bill.","example":2},"amount_minor":{"type":"integer","minimum":0,"description":"What this line cost, in currency minor units; informational.","example":900}}}}},"required":["customer"]},"example":{"customer":{"member_code":"48213906574"},"amount_paid_minor":5000,"excluded_minor":0,"bill_reference":"DEMO-BILL-001"}}}},"tags":["Earning"],"description":"Record a bill and award server-calculated points. Require a stable Idempotency-Key. Amount and bill reference requirements follow loyalty.bill_amount_required and loyalty.bill_reference_required from GET \/config; both default to true. Optional omitted amounts count as zero spend and optional bill references remain null. Staff limits and business-local daily allowances apply; retries do not consume another award allowance. In enforced mode, the current branch requires loyalty.points.award. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization. Award admission is shared across this staff account\u2019s API and dashboard till, across devices and branches in the business. New accepted bills count toward the daily allowance even when they earn zero points; identical retries do not count again. Limited accounts default to a 100000-minor-unit bill maximum and 200 awards per business-local day, configurable by the platform. Legacy\/shadow managers are exempt from these staff limits; in enforced mode only loyalty.points.award_unlimited grants that exemption, independently of role. The separate platform-wide supported-purchase maximum still applies. Limits are enforced by the server and are not a separate capability alias. The recorded source is always pos. Point holds and expiry start from server processing time, not a supplied backdated occurred_at. Till-generated bill_reference strings of at most 100 characters are accepted. Keep the reference, payload and Idempotency-Key unchanged on retries. Purchase bill uniqueness is business + branch with no daily reset; voucher redemption references have no uniqueness constraint. A generated reference does not identify duplicate entry of a physical bill."}},"\/points\/award":{"post":{"operationId":"awardPoints","summary":"Record a bill and calculate points; amount is in minor units","responses":{"201":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Award"}}}},"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":{"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"},"examples":{"missing_bill_fields":{"summary":"Required bill fields missing","value":{"error":{"code":"invalid","message":"Some fields are invalid.","fields":{"amount":["The amount field is required."],"bill_reference":["The bill reference field is required."]}}},"description":"Returned when the business requires both fields and the request omits them. If only one field is required, only that missing field is reported."},"invalid_amount":{"summary":"Bill amount must be positive","value":{"error":{"code":"invalid","message":"Some fields are invalid.","fields":{"amount":["The amount field must be at least 1."]}}}},"award_limit_exceeded":{"summary":"Bill exceeds staff award limit","value":{"error":{"code":"award_limit_exceeded","message":"This bill exceeds the staff award limit."}}}}}}},"429":{"$ref":"#\/components\/responses\/Error429"},"200":{"description":"Success","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Award"}}}},"503":{"$ref":"#\/components\/responses\/Error503"}},"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":{"customer":{"allOf":[{"$ref":"#\/components\/schemas\/LookupCustomer"}],"description":"Lookup object containing exactly one member_code, phone or email. Phone can create a provisional till customer on a purchase; member_code\/email must resolve an existing customer."},"amount":{"type":"integer","minimum":1,"description":"Bill value in integer currency minor units, not points to grant. The loyalty rules calculate points from this value; staff award limits also apply. Required only when GET \/config returns loyalty.bill_amount_required: true (the default). Otherwise it may be omitted or null and is evaluated as zero spend. A supplied value must satisfy this field's integer\/minimum constraints; staff limits still apply.","example":5000,"nullable":true},"branch_id":{"type":"integer","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.","example":12},"bill_reference":{"type":"string","maxLength":100,"description":"Stable receipt\/bill reference for the earning purchase in this branch, up to 100 characters. Keep the same value on retries and later refunds; it is separate from Idempotency-Key. Required only when GET \/config returns loyalty.bill_reference_required: true (the default); otherwise omission or null is accepted. A provided value must be a string of at most 100 characters. Without a reference, reference-based refunds and duplicate-bill detection are unavailable; idempotency remains required.","example":"DEMO-BILL-001","nullable":true},"lines":{"type":"array","maxItems":200,"description":"Optional bill lines for item rules (points per item bought, bill contains items). Each line needs a sku or a category. A refund by amount reduces quantities in proportion to what is still paid, rounded down. Repeating an Idempotency-Key with different lines is a conflict.","items":{"type":"object","required":["quantity"],"properties":{"sku":{"type":"string","maxLength":64,"description":"Item code as the POS knows it; required when category is absent.","example":"LATTE"},"category":{"type":"string","maxLength":64,"description":"Item category as the POS knows it; required when sku is absent.","example":"coffee"},"quantity":{"type":"integer","minimum":1,"maximum":10000,"description":"Units of this item on the bill.","example":2},"amount_minor":{"type":"integer","minimum":0,"description":"What this line cost, in currency minor units; informational.","example":900}}}}},"required":["customer"]},"examples":{"required_bill_fields":{"summary":"Default policy: both fields required","value":{"customer":{"member_code":"48213906574"},"amount":5000,"bill_reference":"DEMO-BILL-001"}},"optional_bill_fields":{"summary":"Business allows both bill fields to be omitted","description":"Use only after config returns both requirement flags as false. Existing per-visit rules may earn points; spend-based rules evaluate zero spend. Keep a stable Idempotency-Key.","value":{"customer":{"member_code":"48213906574"}}}}}}},"tags":["Earning"],"description":"Record a bill using the mobile award alias. The amount field is an integer in currency minor units, not a client-selected points total. Uses the same safeguards and limits as \/purchases. In enforced mode, the current branch requires loyalty.points.award. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization. Award admission is shared across this staff account\u2019s API and dashboard till, across devices and branches in the business. New accepted bills count toward the daily allowance even when they earn zero points; identical retries do not count again. Limited accounts default to a 100000-minor-unit bill maximum and 200 awards per business-local day, configurable by the platform. Legacy\/shadow managers are exempt from these staff limits; in enforced mode only loyalty.points.award_unlimited grants that exemption, independently of role. The separate platform-wide supported-purchase maximum still applies. Limits are enforced by the server and are not a separate capability alias. The business controls whether amount and bill_reference are mandatory through Loyalty Settings. Both default to required. Discover the award form requirements through GET \/config at loyalty.bill_amount_required and loyalty.bill_reference_required. Missing required fields return 422 invalid with messages in error.fields. When a field is optional, omission, null or an empty value is accepted. Omitted amount counts as zero spend: visit rules, eligible fixed bonuses and zero-spend bands can still earn; spend-based points and minimum-spend conditions use zero. A supplied amount must remain a positive integer. No bill reference is invented. The server reads the current saved policy for every new submission; request flags cannot override it. Existing successful retries retain their original result without awarding again."}},"\/refunds":{"post":{"operationId":"refundBill","summary":"Cumulative bill refund; preserves milestone\/reward rules","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"points_removed":{"type":"integer","description":"Points removed as a result of applying this cumulative bill refund."},"points_written_off":{"type":"integer","description":"Points of points_removed the balance could not cover because the customer had already spent them; recorded on the refund entry as written off."},"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."},"rewards_revoked":{"type":"array","items":{"$ref":"#\/components\/schemas\/StaffGrant"},"description":"Unused rewards revoked because the refund removed their earning entitlement."},"rewards_written_off":{"type":"array","items":{"$ref":"#\/components\/schemas\/StaffGrant"},"description":"Already used rewards recorded as written off when the refund removes their earning entitlement."}}}}}},"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":{"description":"Validation or domain refusal, including refund_too_large and loyalty_action_not_allowed when the original earn was already reversed.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"loyalty_action_not_allowed","message":"The points for this bill were already reversed."}}}}},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"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":{"bill_reference":{"type":"string","description":"Exact original purchase bill reference in this branch. Refunds apply to that bill; do not send the refund receipt number.","example":"DEMO-BILL-001","maxLength":100},"refunded_total_minor":{"type":"integer","minimum":0,"description":"Cumulative total refunded for the original bill, in currency minor units, not the amount of this refund alone. If \u00a35 was refunded then another \u00a33, send 800 for GBP. Must not exceed the original bill.","example":1500},"branch_id":{"type":"integer","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.","example":12}},"required":["bill_reference","refunded_total_minor"]},"example":{"bill_reference":"DEMO-BILL-001","refunded_total_minor":1500}}}},"tags":["Corrections"],"description":"Submit the cumulative refunded amount for a bill, not the incremental amount of this refund. The domain recalculates spending, points and earned rewards according to the published policy. In enforced mode, the current branch requires loyalty.refunds.process. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization. A bill whose earn entry has already been reversed cannot be refunded; returns 422 loyalty_action_not_allowed. Refunds and full earn reversals must not correct the same bill twice."}},"\/rewards":{"get":{"operationId":"listStaffRewards","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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"parameters":[{"name":"branch_id","in":"query","required":false,"schema":{"type":"integer"},"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.","example":12},{"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":"member_code","in":"query","required":false,"description":"Optional scanned membership code for personalized eligibility. Without it, catalogue entries are informational and have no claim action.","example":"48213906574","schema":{"type":"string","pattern":"^[0-9]{11}$"}}],"tags":["Rewards"],"description":"List the active points-purchase catalogue for the business, with pagination. This is not the list of rewards already owned by a customer. In enforced mode, the current branch requires loyalty.rewards.view. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization. Anonymous customers must create their voucher in their authenticated browser card. Store\/POS membership-only claims are refused; discovery returns customer_confirmation_required with no claim action. This does not change verified-customer staff claims."}},"\/rewards\/purchase":{"post":{"operationId":"purchaseCustomerReward","summary":"Needs the member code; verified customers only","responses":{"201":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"reward":{"$ref":"#\/components\/schemas\/StaffGrant"},"transaction_id":{"type":"string","minLength":26,"maxLength":26,"nullable":true,"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."}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"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":{"member_code":{"type":"string","description":"Private 11-digit member code scanned from the customer card. Required to authorize spending points; a phone\/email is insufficient.","example":"48213906574"},"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"},"branch_id":{"type":"integer","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.","example":12}},"required":["member_code","reward"]},"example":{"member_code":"48213906574","reward":"01JB0000000000000000000001"}}}},"tags":["Rewards"],"description":"Purchase a points-based reward for a verified customer using the member code. Phone or email alone cannot authorize redemption. Requires an Idempotency-Key. In enforced mode, the current branch requires loyalty.rewards.purchase. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization. Store creation never returns the reward code. Get the code from the customer before redemption. 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. The store\/POS response never returns a reward code. Anonymous customers must create their voucher in their authenticated browser card. Store\/POS membership-only claims are refused; discovery returns customer_confirmation_required with no claim action. This does not change verified-customer staff claims."}},"\/rewards\/validate":{"post":{"operationId":"validateRewardCode","summary":"Read-only validation","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"reward":{"$ref":"#\/components\/schemas\/StaffGrant"},"usable":{"type":"boolean","description":"Whether the current validation permits use. Redeem still rechecks account, expiry, branch and grant state; do not treat this flag as a reservation."},"reason_code":{"type":"string","nullable":true,"description":"Canonical refusal reason shared with redemption; null when usable."},"message":{"type":"string","nullable":true,"description":"Human-readable refusal reason."},"snapshot":{"type":"boolean","description":"Always true: this read-only result does not reserve the voucher.","example":true}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"reward_code":{"type":"string","description":"Private code of an issued customer reward, not the catalogue reward ID or membership code. Scan\/type the customer-provided code.","example":"DEMO7KQ2M","maxLength":12},"branch_id":{"type":"integer","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.","example":12}},"required":["reward_code"]},"example":{"reward_code":"DEMO7KQ2M"}}}},"tags":["Rewards"],"description":"Check a reward code and branch eligibility without consuming it. Validation does not reserve the reward; final use can still fail if its state changes. In enforced mode, the current branch requires loyalty.rewards.validate. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization. Uses the same eligibility policy as locked redemption, including membership, program, branch and pending refund effects. Returns a read-only snapshot, not a reservation. An anonymously enrolled customer may present an issued voucher: current program and reward anonymous-redemption permissions must both be enabled, or the customer must first verify a recovery contact. Scan or enter the voucher code; a membership code alone does not redeem it."}},"\/rewards\/use":{"post":{"operationId":"useRewardCode","summary":"Atomic reward use with a bill number","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"reward":{"$ref":"#\/components\/schemas\/StaffGrant"},"transaction_id":{"type":"string","minLength":26,"maxLength":26,"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."}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"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_code":{"type":"string","description":"Private code of an issued customer reward, not the catalogue reward ID or membership code. Scan\/type the customer-provided code.","example":"DEMO7KQ2M","maxLength":12},"bill_reference":{"type":"string","description":"Optional receipt reference for reconciliation, at most 100 characters. Omitted, null or empty is accepted independently of purchase bill_reference_required.","example":"DEMO-REDEEM-001","maxLength":100,"nullable":true},"branch_id":{"type":"integer","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.","example":12}},"required":["reward_code"]},"example":{"reward_code":"DEMO7KQ2M","bill_reference":"DEMO-REDEEM-001"}}}},"tags":["Rewards"],"description":"Atomically consume a reward code with an optional bill_reference. Requires a verified customer and an eligible branch. Preserve the Idempotency-Key on retries. In enforced mode, the current branch requires loyalty.rewards.redeem. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization. An anonymously enrolled customer may present an issued voucher: current program and reward anonymous-redemption permissions must both be enabled, or the customer must first verify a recovery contact. Scan or enter the voucher code; a membership code alone does not redeem it. Bill reference is optional for earned-voucher redemption; do not require staff to enter or invent one. Idempotency-Key remains mandatory."}},"\/vouchers\/{code}":{"get":{"operationId":"getVoucher","summary":"Voucher, masked owner and branch policy","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"reward":{"$ref":"#\/components\/schemas\/StaffGrant"},"usable":{"type":"boolean","description":"Whether the current validation permits use. Redeem still rechecks account, expiry, branch and grant state; do not treat this flag as a reservation."},"owner":{"type":"object","properties":{"name":{"type":"string","nullable":true,"description":"Display name of the object represented here; do not use it as a stable identifier."},"phone":{"type":"string","nullable":true,"description":"Phone number. International E.164 format is recommended; customer inputs are normalized using the business country."}},"description":"Masked customer name and phone for confirmation; not the full contact details."},"branch_ids":{"type":"array","items":{"type":"integer"},"description":"Branch restrictions for this reward. An empty list means no configured branch restriction."},"reason_code":{"type":"string","nullable":true,"description":"Canonical refusal reason shared with redemption; null when usable."},"message":{"type":"string","nullable":true,"description":"Human-readable refusal reason."},"snapshot":{"type":"boolean","description":"Always true: this read-only result does not reserve the voucher.","example":true}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string"},"description":"Private issued reward\/voucher code shown by the customer, not a catalogue ULID or member code. The server trims whitespace and normalizes to uppercase.","example":"DEMO-REWARD"},{"name":"branch_id","in":"query","required":false,"schema":{"type":"integer"},"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.","example":12}],"tags":["Vouchers"],"description":"Inspect a voucher by code, including masked owner and branch policy. Handle voucher_not_found, voucher_already_used and eligibility refusals. In enforced mode, the current branch requires loyalty.rewards.validate. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization. Uses the same eligibility policy as locked redemption, including membership, program, branch and pending refund effects. Returns a read-only snapshot, not a reservation. An anonymously enrolled customer may present an issued voucher: current program and reward anonymous-redemption permissions must both be enabled, or the customer must first verify a recovery contact. Scan or enter the voucher code; a membership code alone does not redeem it. Accepts codes issued by customer POST \/api\/app\/v1\/rewards\/create-voucher. Check usable and reason_code before offering redemption; redemption rechecks eligibility."}},"\/vouchers\/{code}\/redeem":{"post":{"operationId":"redeemVoucher","summary":"Atomic voucher use; optional bill reference and separate voucher errors","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"reward":{"$ref":"#\/components\/schemas\/StaffGrant"},"transaction_id":{"type":"string","minLength":26,"maxLength":26,"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."}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string"},"description":"Private issued reward\/voucher code shown by the customer, not a catalogue ULID or member code. The server trims whitespace and normalizes to uppercase.","example":"DEMO-REWARD"},{"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":{"bill_reference":{"type":"string","maxLength":100,"description":"Optional receipt reference for reconciliation, at most 100 characters. Omitted, null or empty is accepted independently of purchase bill_reference_required.","example":"DEMO-REDEEM-001","nullable":true},"branch_id":{"type":"integer","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.","example":12}}},"example":{"bill_reference":"DEMO-REDEEM-001"}}}},"tags":["Vouchers"],"description":"Atomically redeem a voucher and record its optional bill_reference. A concurrent or previously completed use returns voucher_already_used; do not retry using a new operation key. In enforced mode, the current branch requires loyalty.rewards.redeem. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization. An anonymously enrolled customer may present an issued voucher: current program and reward anonymous-redemption permissions must both be enabled, or the customer must first verify a recovery contact. Scan or enter the voucher code; a membership code alone does not redeem it. Till-generated bill_reference strings of at most 100 characters are accepted. Keep the reference, payload and Idempotency-Key unchanged on retries. Purchase bill uniqueness is business + branch with no daily reset; voucher redemption references have no uniqueness constraint. A generated reference does not identify duplicate entry of a physical bill. Bill reference is optional for earned-voucher redemption; do not require staff to enter or invent one. Idempotency-Key remains mandatory."}},"\/transactions\/{id}\/reverse":{"post":{"operationId":"reverseTransaction","summary":"Reverse an eligible transaction or void a voucher use with current target-specific authorization","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"reversed":{"type":"boolean","description":"True when the requested reversal completed."},"reason":{"type":"string","description":"Human-readable reason for the correction, retained in the audit trail."}}}}}},"400":{"$ref":"#\/components\/responses\/Error400"},"401":{"$ref":"#\/components\/responses\/Error401"},"403":{"description":"Current capability or exact target permission is denied; refresh configuration. A saved successful response does not bypass revocation.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"forbidden","message":"You do not have permission for this reversal."}}}}},"404":{"$ref":"#\/components\/responses\/Error404"},"409":{"$ref":"#\/components\/responses\/Error409"},"422":{"description":"Validation or domain refusal: refund_required for milestone-moving earns; loyalty_action_not_allowed for a refunded earn or another ineligible correction.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"loyalty_action_not_allowed","message":"This bill has already been refunded. Correct it through the bill refund workflow."}}}}},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":26,"maxLength":26},"description":"Public transaction ULID from the original write result in the same business and branch. Legacy\/shadow mode requires a manager; enforced mode checks the exact earn\/adjust, reward-revoke or voucher-use-void permission, including on replay. Milestone-moving earns require the bill refund workflow.","example":"01JB0000000000000000000001"},{"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":{"reason":{"type":"string","maxLength":255,"description":"Human-readable reason for the correction, retained in the audit trail.","example":"Duplicate entry confirmed by manager"},"branch_id":{"type":"integer","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.","example":12}},"required":["reason"]},"example":{"reason":"Duplicate entry confirmed by manager"}}}},"tags":["Corrections"],"description":"Correction with a required reason and public transaction ULID at the current authorized branch. Legacy\/shadow mode requires a business manager. Enforced mode checks the exact target: loyalty.entries.reverse for earn\/adjust entries, loyalty.rewards.revoke for a points-purchased reward debit, or loyalty.rewards.void_use for a particular voucher-use audit transaction. The transactions.reverse capability means at least one reversal kind is allowed; it does not authorize every target. Current identity, branch and exact target permission are rechecked before returning an idempotent replay, so a previously successful key may now return 403 after revocation. Milestone-moving earns require the bill refund workflow (422 refund_required). Other eligible earns with any bill refund already recorded cannot also be reversed (422 loyalty_action_not_allowed); use the bill refund workflow for further permitted correction."}},"\/activity":{"get":{"operationId":"getStaffActivity","summary":"Own successful audited actions","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"actions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":26,"maxLength":26,"description":"Identifier of this object. Preserve the documented type and use the ID rather than a display name."},"action":{"type":"string","description":"Activity display action. purchase includes purchases and points\/award; voucher_redeem includes vouchers\/{code}\/redeem and rewards\/use. Other names match operation aliases.","enum":["purchase","voucher_redeem","customers.create","refunds","rewards.purchase","rewards.create-voucher","transactions.reverse","wallet.send"]},"branch_id":{"type":"integer","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."},"device":{"type":"string","description":"Installation details used to issue a device token and register optional push delivery."},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 creation timestamp of the record."},"transaction_id":{"type":"string","minLength":26,"maxLength":26,"nullable":true,"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."},"customer":{"type":"object","nullable":true,"properties":{"name":{"type":"string","description":"Masked current display name, e.g. S***."}},"description":"Customer with a masked current display name, or null when unavailable."},"amount_minor":{"type":"integer","nullable":true,"description":"Submitted purchase amount in business currency minor units, including zero-point purchases."},"points":{"type":"integer","nullable":true,"description":"Points awarded for a purchase; zero when none were awarded."},"bill_reference":{"type":"string","nullable":true,"maxLength":100,"description":"Recorded bill reference, or null when inapplicable or no longer retained."},"reward":{"type":"object","nullable":true,"properties":{"name":{"type":"string","description":"Reward name at the time of the recorded operation."}},"description":"Reward display metadata, or null when inapplicable or no longer retained."}}},"description":"Page of audit operations performed by the authenticated staff member."},"meta":{"$ref":"#\/components\/schemas\/Meta"}}},"example":{"actions":[{"id":"01JF0000000000000000000002","action":"voucher_redeem","branch_id":1,"device":"Till 2","created_at":"2026-10-30T10:14:00+00:00","transaction_id":"01JF0000000000000000000002","customer":{"name":"S***"},"amount_minor":null,"points":null,"bill_reference":"TILL2-20261030-101400-R7KQ","reward":{"name":"Free coffee or tea"}},{"id":"01JF0000000000000000000001","action":"purchase","branch_id":1,"device":"Till 2","created_at":"2026-10-30T10:12:00+00:00","transaction_id":"01JD0000000000000000000001","customer":{"name":"S***"},"amount_minor":346,"points":3,"bill_reference":"20431","reward":null}],"meta":{"current_page":1,"last_page":1,"per_page":50,"total":2}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"parameters":[{"name":"branch_id","in":"query","required":false,"schema":{"type":"integer"},"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.","example":12},{"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":"date","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"YYYY-MM-DD in \/config.business.timezone. Invalid dates return 422 invalid. Omit for all dates.","example":"2026-10-30"}],"tags":["Activity & messaging"],"description":"Own successful actions at the selected authorized branch, newest created_at first (ID breaks ties). Optional date selects a business-local calendar day, including daylight-saving changes. Details are nullable when inapplicable or the encrypted customer snapshot has been erased. Development records without request metadata may have null amount_minor or bill_reference."}},"\/wallet\/send":{"post":{"operationId":"sendWalletLink","summary":"Queue private link to the customer on their reachable channel","responses":{"202":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean","description":"True when asynchronous delivery work was accepted. It is not a delivery receipt or a guarantee that every recipient is reachable."}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"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":{"member_code":{"type":"string","pattern":"^\\d{11}$","description":"Customer membership QR\/card value: exactly 11 digits, sent as a string. Supply exactly one of member_code, phone or email. Store lookup never returns reward codes, even after a membership scan.","example":"48213906574"},"phone":{"type":"string","description":"Customer phone for an earning-only lookup. E.164 is recommended; local numbers use the business country. Supply exactly one lookup identifier.","example":"+447700900123","maxLength":30},"email":{"type":"string","format":"email","description":"Customer email for an earning-only lookup. Supply exactly one lookup identifier; email lookup never authorizes redemption.","example":"customer@example.com","maxLength":255},"branch_id":{"type":"integer","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.","example":12}},"description":"Supply exactly one of member_code, phone or email. The wallet link is delivered privately to the customer, not returned to staff."},"example":{"member_code":"48213906574"}}}},"tags":["Activity & messaging"],"description":"Queue a private wallet link to an identified customer\u2019s reachable channel. Channel and integration availability are checked by the server. Acceptance does not guarantee delivery. In enforced mode, the current branch requires loyalty.wallet_links.send. Legacy\/shadow behavior and business feature requirements continue to apply. Capability aliases are UI hints; the server rechecks current authorization."}},"\/config":{"get":{"operationId":"staffConfiguration","summary":"Store App configuration, schema version 1","description":"Authenticated business defaults and current branch capabilities. Managers may omit branch_id and use a valid assigned branch as legacy fallback; without one selected_branch is null. No branded customer-app provisioning information. All fields are required; additive optional fields and unknown capability identifiers must be ignored. Breaking semantics require a new version. Operation aliases in staff.permissions and \/config.capabilities reflect current branch scope, business access and authorization mode. Do not infer allowed operations from role alone; enforced mode supports grants to staff and denies to managers. Refresh after a branch change or 403. Authorization lookup failures can return 503 authorization_unavailable; honor Retry-After. When capabilities includes points.award, loyalty.bill_amount_required and loyalty.bill_reference_required are required booleans reflecting the business's current Loyalty Settings, both defaulting to true. They describe the amount and bill_reference fields of POST \/points\/award; business managers can change each requirement independently. The flags do not grant operation permissions. Programme state and requirements are present independently of points.award permission and branch selection. loyalty.available is false while the programme is paused; loyalty is null only when the business lacks the loyalty feature. Use the flags for required indicators and client validation, refresh config after branch changes, and show a retry state if config cannot be loaded rather than treating missing flags as optional fields.","tags":["Store App"],"parameters":[{"name":"branch_id","in":"query","schema":{"type":"integer"},"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. Omit if no selection is needed; a manager may receive no active branch until one is selected.","example":12}],"responses":{"200":{"description":"Current Store App configuration","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/StoreConfiguration"},"examples":{"awarding_available":{"summary":"Awarding available","description":"An authorized manager has selected a branch and can award points. Both bill inputs are mandatory. Identifiers are fictional.","value":{"schema_version":1,"input_profile":{"country_code":"AE","country_name":"United Arab Emirates","dialing_code":"+971","address_format":null,"fields":{"phone":{"label":"Phone Number","placeholder":"+971 50 123 4567"},"city":{"label":"City","placeholder":"City or town","suggestions":[]},"area":{"label":"Area","placeholder":"Neighbourhood or area","suggestions":[]},"province":{"label":"Emirate","placeholder":"Emirate","suggestions":[]},"postal_code":{"label":"Postal Code","placeholder":"Postal code"},"address":{"label":"Address","placeholder":"Building and street address"}}},"customer_identifier_types":["member_code","phone","email"],"loyalty":{"points_label":"points","redemption_model":"catalogue","bill_amount_required":true,"bill_reference_required":true,"available":true},"business":{"id":1,"public_id":"01J00000000000000000000001","name":"Example Business","currency":"AED","locale":"en_AE","timezone":"Asia\/Dubai"},"staff":{"id":7,"public_id":"01J00000000000000000000007","name":"Example Manager","role":"business_admin"},"selected_branch":{"id":12,"public_id":"01J00000000000000000000012","name":"Example Branch"},"branch_required":false,"can_select_branch":true,"capabilities":["customers.lookup","points.award","purchases.preview","purchases","activity"],"unavailable_reason":null}},"branch_required":{"summary":"Branch selection required","description":"Select an authorized branch and reload config before showing the award form. Programme state remains visible without a branch; capabilities remain empty. Identifiers are fictional.","value":{"schema_version":1,"input_profile":{"country_code":"AE","country_name":"United Arab Emirates","dialing_code":"+971","address_format":null,"fields":{"phone":{"label":"Phone Number","placeholder":"+971 50 123 4567"},"city":{"label":"City","placeholder":"City or town","suggestions":[]},"area":{"label":"Area","placeholder":"Neighbourhood or area","suggestions":[]},"province":{"label":"Emirate","placeholder":"Emirate","suggestions":[]},"postal_code":{"label":"Postal Code","placeholder":"Postal code"},"address":{"label":"Address","placeholder":"Building and street address"}}},"customer_identifier_types":["member_code","phone","email"],"loyalty":{"available":true,"points_label":"points","redemption_model":"catalogue","bill_amount_required":true,"bill_reference_required":true},"business":{"id":1,"public_id":"01J00000000000000000000001","name":"Example Business","currency":"AED","locale":"en_AE","timezone":"Asia\/Dubai"},"staff":{"id":7,"public_id":"01J00000000000000000000007","name":"Example Manager","role":"business_admin"},"selected_branch":null,"branch_required":true,"can_select_branch":true,"capabilities":[],"unavailable_reason":"branch_required"}},"awarding_unavailable":{"summary":"Awarding unavailable","description":"The programme is active, but this staff member has no operation grants at the selected branch. Hide the award action and retain identity, logout and authorized branch discovery. Identifiers are fictional.","value":{"schema_version":1,"input_profile":{"country_code":"AE","country_name":"United Arab Emirates","dialing_code":"+971","address_format":null,"fields":{"phone":{"label":"Phone Number","placeholder":"+971 50 123 4567"},"city":{"label":"City","placeholder":"City or town","suggestions":[]},"area":{"label":"Area","placeholder":"Neighbourhood or area","suggestions":[]},"province":{"label":"Emirate","placeholder":"Emirate","suggestions":[]},"postal_code":{"label":"Postal Code","placeholder":"Postal code"},"address":{"label":"Address","placeholder":"Building and street address"}}},"customer_identifier_types":["member_code","phone","email"],"loyalty":{"points_label":"points","redemption_model":"catalogue","bill_amount_required":true,"bill_reference_required":true,"available":true},"business":{"id":1,"public_id":"01J00000000000000000000001","name":"Example Business","currency":"AED","locale":"en_AE","timezone":"Asia\/Dubai"},"staff":{"id":7,"public_id":"01J00000000000000000000007","name":"Example Manager","role":"business_admin"},"selected_branch":{"id":12,"public_id":"01J00000000000000000000012","name":"Example Branch"},"branch_required":false,"can_select_branch":true,"capabilities":[],"unavailable_reason":"no_available_operations"}},"optional_bill_fields":{"summary":"Optional bill fields","description":"The manager disabled both field requirements in Loyalty Settings. Staff may omit them; the server still applies the active earning rules, idempotency and admission limits. Each setting can also be changed independently.","value":{"schema_version":1,"input_profile":{"country_code":"AE","country_name":"United Arab Emirates","dialing_code":"+971","address_format":null,"fields":{"phone":{"label":"Phone Number","placeholder":"+971 50 123 4567"},"city":{"label":"City","placeholder":"City or town","suggestions":[]},"area":{"label":"Area","placeholder":"Neighbourhood or area","suggestions":[]},"province":{"label":"Emirate","placeholder":"Emirate","suggestions":[]},"postal_code":{"label":"Postal Code","placeholder":"Postal code"},"address":{"label":"Address","placeholder":"Building and street address"}}},"customer_identifier_types":["member_code","phone","email"],"loyalty":{"points_label":"points","redemption_model":"catalogue","bill_amount_required":false,"bill_reference_required":false,"available":true},"business":{"id":1,"public_id":"01J00000000000000000000001","name":"Example Business","currency":"AED","locale":"en_AE","timezone":"Asia\/Dubai"},"staff":{"id":7,"public_id":"01J00000000000000000000007","name":"Example Manager","role":"business_admin"},"selected_branch":{"id":12,"public_id":"01J00000000000000000000012","name":"Example Branch"},"branch_required":false,"can_select_branch":true,"capabilities":["customers.lookup","points.award","purchases.preview","purchases","activity"],"unavailable_reason":null}},"rewards_paused":{"summary":"Rewards paused","description":"The business paused its programme. Display the paused state independently of operation capabilities. Identifiers are fictional.","value":{"schema_version":1,"input_profile":{"country_code":"AE","country_name":"United Arab Emirates","dialing_code":"+971","address_format":null,"fields":{"phone":{"label":"Phone Number","placeholder":"+971 50 123 4567"},"city":{"label":"City","placeholder":"City or town","suggestions":[]},"area":{"label":"Area","placeholder":"Neighbourhood or area","suggestions":[]},"province":{"label":"Emirate","placeholder":"Emirate","suggestions":[]},"postal_code":{"label":"Postal Code","placeholder":"Postal code"},"address":{"label":"Address","placeholder":"Building and street address"}}},"customer_identifier_types":["member_code","phone","email"],"loyalty":{"points_label":"points","redemption_model":"catalogue","bill_amount_required":true,"bill_reference_required":true,"available":false},"business":{"id":1,"public_id":"01J00000000000000000000001","name":"Example Business","currency":"AED","locale":"en_AE","timezone":"Asia\/Dubai"},"staff":{"id":7,"public_id":"01J00000000000000000000007","name":"Example Manager","role":"business_admin"},"selected_branch":{"id":12,"public_id":"01J00000000000000000000012","name":"Example Branch"},"branch_required":false,"can_select_branch":true,"capabilities":["customers.lookup","points.award","purchases.preview","purchases","activity"],"unavailable_reason":null}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}}}},"\/branches":{"get":{"operationId":"staffBranches","summary":"Discover authorized active branches","description":"Paginated tenant-scoped discovery; branch staff see only their assignment. Managers select per native request; browser managers use CSRF-protected branch-select. Does not mutate a global selection.","tags":["Store App"],"parameters":[{"name":"page","in":"query","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","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Number of records requested per page. Defaults to 20; minimum 1 and maximum 100. Use pagination metadata to retrieve further pages.","example":20}],"responses":{"200":{"description":"A bounded page of authorized active branches","content":{"application\/json":{"schema":{"type":"object","required":["branches","meta"],"properties":{"branches":{"type":"array","items":{"$ref":"#\/components\/schemas\/StoreBranch"},"description":"Branches included in this response. Follow pagination where supplied rather than assuming the list is complete."},"meta":{"type":"object","required":["current_page","last_page","per_page","total"],"properties":{"current_page":{"type":"integer","description":"One-based page number returned for this request."},"last_page":{"type":"integer","description":"Last available page number for the current filters."},"per_page":{"type":"integer","description":"Maximum number of records included per page, subject to the endpoint limit."},"total":{"type":"integer","description":"Total matching records across all pages, not just the current page."}},"description":"Pagination metadata for this result set."}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"}}}},"\/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\/CreatedVoucher"}}}}}},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"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"},"member_code":{"type":"string","pattern":"^[0-9]{11}$","description":"Scanned customer membership code. Phone or email cannot authorize issuance.","example":"48213906574"}},"required":["entitlement","member_code"]},"example":{"entitlement":"01JB0000000000000000000001","member_code":"48213906574"}}}},"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. Requires a membership code and loyalty.rewards.purchase permission. The response never contains a redemption code, including retries. Customer must read the code in their own app and share it for validation\/redemption. 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. Anonymous customers must create their voucher in their authenticated browser card. Store\/POS membership-only claims are refused; discovery returns customer_confirmation_required with no claim action. This does not change verified-customer staff claims."}},"\/customers\/rewards":{"get":{"operationId":"listCustomerRewards","summary":"Paginated owned rewards and server-calculated claim actions","responses":{"200":{"description":"Success","content":{"application\/json":{"schema":{"type":"object","properties":{"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."},"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"},"429":{"$ref":"#\/components\/responses\/Error429"},"503":{"$ref":"#\/components\/responses\/Error503"}},"parameters":[{"name":"branch_id","in":"query","required":false,"schema":{"type":"integer"},"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.","example":12},{"name":"member_code","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{11}$"},"description":"Customer membership QR\/card value: exactly 11 digits, sent as a string. Supply exactly one of member_code, phone or email. Store lookup never returns reward codes, even after a membership scan.","example":"48213906574"},{"name":"phone","in":"query","required":false,"schema":{"type":"string"},"description":"Customer phone for an earning-only lookup. E.164 is recommended; local numbers use the business country. Supply exactly one lookup identifier.","example":"+447700900123"},{"name":"email","in":"query","required":false,"schema":{"type":"string","format":"email"},"description":"Customer email for an earning-only lookup. Supply exactly one lookup identifier; email lookup never authorizes redemption.","example":"customer@example.com"},{"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,"description":"Filter reward lifecycle state. Omit to list earned and available rewards.","example":"earned","schema":{"type":"string","enum":["earned","available","used","expired","revoked"]}}],"tags":["Rewards"],"description":"Identify a customer using exactly one member_code, phone or email within the authorized business and branch. Returns paginated earned\/available rewards by default, or a requested status. No reward codes are ever returned. Phone\/email discovery is informational; claiming requires the membership code. Requires customers.rewards capability mapped to loyalty.members.lookup. Claim decisions include business settings and actor permissions and are rechecked at submission. Anonymous customers must create their voucher in their authenticated browser card. Store\/POS membership-only claims are refused; discovery returns customer_confirmation_required with no claim action. This does not change verified-customer staff claims."}}},"components":{"securitySchemes":{"Bearer":{"type":"http","scheme":"bearer","description":"Token returned by POST \/login for a personal staff device. The token determines the business and staff scope; do not send a customer app ID or POS key."}},"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."}},"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."},"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."}},"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. Returned only after a member-code lookup; phone and email lookups omit it because it authorizes buying rewards with points."},"balance":{"type":"integer","example":10,"description":"Current points balance. Use spendable, when provided, to decide how many points can be redeemed."},"spendable":{"type":"integer","example":10,"description":"Points currently available to spend, excluding points on hold."},"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."},"card":{"type":"object","properties":{"number":{"type":"integer","description":"Current milestone card\/cycle number."},"points":{"type":"integer","example":2,"description":"Points earned, moved or accumulated in this context; these are loyalty points, not a currency amount."},"milestones":{"type":"array","items":{"type":"object","properties":{"points":{"type":"integer","example":2,"description":"Points threshold on this card at which the milestone reward is unlocked."}}},"description":"Configured points thresholds for the milestone card."}},"nullable":true,"description":"Loyalty card branding or milestone progress, depending on this response."}},"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"},"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. Lookup and voucher creation omit the code. Validation\/redemption use a code supplied by the customer."},"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."}},"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":"Account role\/category, not a complete list of allowed actions. Legacy\/shadow mode uses legacy role rules; enforced mode can delegate actions to staff or deny actions to managers. Branch confinement always applies."},"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 aliases for the authenticated staff\/branch context, not RBAC permission-code names. Reflect current business features and authorization mode. Use for UI hints rather than role checks; refresh after branch changes or 403. A reversal alias still requires exact target authorization and the server rechecks every mutation\/replay."},"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."},"public_id":{"type":"string","minLength":26,"maxLength":26,"description":"Stable public ULID for this object. Keep it distinct from any legacy integer id."}},"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. Store lookup never returns reward codes, even after a membership scan."},"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."},"phone":{"type":"string","nullable":true,"example":"+971501234567","description":"The customer's verified phone in E.164, else the phone they registered with; null when none."}},"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."},"rewards_truncated":{"type":"boolean","description":"True when the legacy lookup summary omits additional rewards. Use GET \/customers\/rewards to page through the full list."}},"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."},"StoreBranch":{"type":"object","required":["id","public_id","name"],"properties":{"id":{"type":"integer","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":"Existing ULID identifier."},"name":{"type":"string","description":"Display name of the object represented here; do not use it as a stable identifier.","example":"Demo High Street"}},"description":"Authorized active branch. Use its integer id for request branch selection."},"StoreConfiguration":{"type":"object","required":["schema_version","business","staff","selected_branch","branch_required","can_select_branch","capabilities","unavailable_reason","input_profile","customer_identifier_types","loyalty"],"properties":{"schema_version":{"type":"integer","enum":[1],"description":"Version of the Store App configuration payload; currently 1."},"business":{"type":"object","required":["id","public_id","name","currency","locale","timezone"],"properties":{"id":{"type":"integer","description":"Legacy integer ID of this business."},"public_id":{"type":"string","description":"Stable public ULID of this business; preserve it separately from the integer id."},"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."},"staff":{"type":"object","required":["id","public_id","name","role"],"properties":{"id":{"type":"integer","description":"Legacy integer ID of this staff account."},"public_id":{"type":"string","description":"Stable public ULID of this staff account; preserve it separately from the integer id."},"name":{"type":"string","description":"Display name of the object represented here; do not use it as a stable identifier."},"role":{"type":"string","description":"Account role\/category, not a complete list of allowed actions. Legacy\/shadow mode uses legacy role rules; enforced mode can delegate actions to staff or deny actions to managers. Branch confinement always applies."}},"description":"Authenticated staff identity and, where provided, authorized branch\/capability information."},"selected_branch":{"type":"object","required":["id","public_id","name"],"properties":{"id":{"type":"integer","description":"Legacy integer ID of this selected branch."},"public_id":{"type":"string","minLength":26,"maxLength":26,"description":"Stable public ULID of this selected branch; preserve it separately from the integer id."},"name":{"type":"string","description":"Display name of the object represented here; do not use it as a stable identifier."}},"nullable":true,"description":"Active branch resolved for this request, or null when a manager still needs to select a branch."},"branch_required":{"type":"boolean","description":"True when no active branch is selected and branch-scoped operations cannot run yet."},"can_select_branch":{"type":"boolean","description":"Whether this staff role may select from the business authorized branches."},"capabilities":{"type":"array","uniqueItems":true,"items":{"type":"string"},"description":"Currently runnable operation identifiers: customers.lookup, customers.rewards, customers.create, points.award, vouchers.show, vouchers.redeem, purchases.preview, purchases, refunds, rewards, rewards.purchase, rewards.create-voucher, rewards.validate, rewards.use, wallet.send, transactions.reverse, activity. Ignore unknown identifiers; they never grant permission. These are operation aliases, not RBAC permission codes. Legacy\/shadow mode preserves legacy rules; enforced mode reflects current grants\/denies at the selected branch. Role alone does not authorize an action. transactions.reverse can represent only one allowed reversal kind, so exact target checks may still deny with 403. Refresh after branch changes or authorization errors. This list does not expose an unlimited-award flag."},"unavailable_reason":{"type":"string","nullable":true,"enum":["branch_required","no_available_operations",null],"description":"Reason no operations are available: branch_required or no_available_operations; null when available."},"loyalty":{"type":"object","nullable":true,"description":"Programme state and award requirements independent of staff permissions and branch selection. Null only when the loyalty feature is unavailable to the business. available=false means the programme is paused; use capabilities separately to decide which staff operations are permitted. Staff purchase previews use the amount policy and never require a bill reference.","properties":{"points_label":{"type":"string","description":"Business-specific display label for points, such as points or beans."},"redemption_model":{"type":"string","enum":["catalogue","milestones"],"description":"Loyalty program model: catalogue exchanges points for rewards; milestones unlocks rewards through card progress."},"bill_amount_required":{"type":"boolean","example":true,"description":"Whether Store App awards require a bill amount, configured per business in Loyalty Settings and defaulting to true. When true, POST \/points\/award requires amount: an integer of at least 1 in currency minor units. When false, amount may be omitted or null; omission counts as zero spend and existing visit\/fixed earning rules may still apply. Provided amounts must still be positive integers. Staff purchases and previews apply the same requirement to amount_paid_minor (minimum 0 when supplied)."},"bill_reference_required":{"type":"boolean","example":true,"description":"Whether Store App awards require a bill reference, configured per business in Loyalty Settings and defaulting to true. When true, POST \/points\/award and staff POST \/purchases require bill_reference: a non-empty string of at most 100 characters. When false, it may be omitted or null. Previews always allow omission. No bill reference is invented; reference-based refunds and duplicate-bill detection require one to have been recorded. Idempotency keys and limits remain mandatory."},"available":{"type":"boolean","description":"Programme availability; false while paused. Does not grant staff permission.","example":true}},"required":["available","points_label","redemption_model","bill_amount_required","bill_reference_required"]},"input_profile":{"$ref":"#\/components\/schemas\/CountryInputProfile"},"customer_identifier_types":{"type":"array","uniqueItems":true,"description":"Supported customer lookup identifiers for this business, with member_code first. Phone and email follow the configured sign-up methods; an independent Store App without a branded customer app supports both. This list does not verify a customer or grant permission to award points.","items":{"type":"string","enum":["member_code","phone","email"]},"example":["member_code","phone","email"]}},"description":"Current Store App identity, selected branch, capabilities and display metadata. Re-fetch after branch changes or authorization errors."},"CreatedVoucher":{"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"},"status":{"type":"string","enum":["available"],"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."},"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":"Voucher creation status and non-secret metadata only. The customer alone receives the code."},"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. For anonymous customers, customer_confirmation_required means the customer must create the voucher in their private browser; verified_contact_required means verification is required by current reward\/program policy.","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"]}}},"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."}}}},"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 that the staff account is active.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"unauthenticated","message":"Authentication required."}}}}},"Error403":{"description":"Access denied \u2014 Check business entitlements, feature availability and staff permissions. Retrying unchanged will not restore access. Capability denial can return operation_unavailable; a missing exact reversal permission can return forbidden. Refresh \/config for the current branch and stop the refused action. An unchanged request is not made valid by using another operation key.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"api_access_unavailable","message":"API access is unavailable."}}}}},"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."]}}}}}},"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"}}}},"Error503":{"description":"Authorization state unavailable \u2014 Retry after the Retry-After delay using bounded backoff. For a write, retain the same Idempotency-Key and identical payload; do not assume the operation succeeded or fall back to role-based access.","headers":{"Retry-After":{"description":"Seconds to wait before retrying authorization resolution; currently 3.","schema":{"type":"integer","minimum":1,"example":3}}},"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"error":{"code":"authorization_unavailable","message":"Access settings are unavailable. Try again shortly."}}}}}}},"tags":[{"name":"Authentication"},{"name":"Customers"},{"name":"Earning"},{"name":"Corrections"},{"name":"Rewards"},{"name":"Vouchers"},{"name":"Activity & messaging"}]}