{"openapi":"3.1.0","info":{"title":"Ta'ab Partner API","version":"1","description":"Issue passes into Ta'ab wallets and redeem them when the holder taps their card or shows the Ta'ab app's QR. Server to server: Basic auth with your client id and secret. No money moves through this API. Guide: /developers."},"servers":[{"url":"https://sandbox.taab.mohbel.com/api/partner/v1"}],"security":[{"clientCredentials":[]}],"paths":{"/identity/resolve":{"post":{"operationId":"resolveIdentity","summary":"Whose card or QR is this?","description":"Tells you whether a credential is a genuine, assigned Ta'ab card or a live Ta'ab QR, and returns the holder's Ta'ab ID. Use it to link one of your users to their Ta'ab account.\n\nNeeds the scope `identity.resolve`. Read-only: writes nothing and never appears in the holder's history.","x-taab-scopes":["identity.resolve"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"credential":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"nfc"},"uid":{"type":"string","minLength":1,"maxLength":64,"description":"The card's UID exactly as the reader reports it. Any spelling is accepted: 04A22B91, 04:a2:2b:91, 04 A2 2B 91."}},"required":["type","uid"]},{"type":"object","properties":{"type":{"type":"string","const":"qr"},"token":{"type":"string","minLength":1,"maxLength":500,"description":"The string decoded from the QR in the Ta'ab app. It rotates every 60 seconds; with no signal the app shows an offline code (o1.…) instead, good for 10 minutes, which Ta'ab takes too."}},"required":["type","token"]}],"description":"What the holder presented: a card tap or the Ta'ab app's QR. Send the raw value, never a hash."}},"required":["credential"]},"example":{"credential":{"type":"nfc","uid":"04:a2:2b:91:51:c2:80"}}}}},"responses":{"200":{"description":"The credential is active, on an active, activated account.","content":{"application/json":{"schema":{"type":"object","properties":{"taabId":{"type":"string","description":"The holder's permanent Ta'ab ID. Store it against your own user."},"accountStatus":{"type":"string","description":"Always active on a 200."},"credentialType":{"type":"string","enum":["nfc","qr"]},"card":{"anyOf":[{"type":"object","properties":{"cardNumber":{"type":"string","description":"The number printed on the card."},"status":{"type":"string","description":"Always assigned on a 200."}},"required":["cardNumber","status"],"additionalProperties":false},{"type":"null"}],"description":"The card behind an NFC tap. null for a QR."}},"required":["taabId","accountStatus","credentialType","card"],"additionalProperties":false,"description":"Who is presenting."},"example":{"taabId":"TB-00000001234","accountStatus":"active","credentialType":"nfc","card":{"cardNumber":"C-0041","status":"assigned"}}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`credential_not_found`: No Ta'ab card has that UID, or the QR is not one Ta'ab issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"credential_not_found","message":"No Ta'ab card has that UID, or the QR is not one Ta'ab issued."}}}},"409":{"description":"`card_not_assigned`: A real Ta'ab card, but in stock, returned, lost or retired.\n\n`credential_stale`: The QR token has been superseded by a newer one. Ask the holder to refresh and scan again.\n\n`credential_revoked`: The QR credential was revoked (a lost phone).\n\n`account_inactive`: The account is suspended, closed or scrubbed.\n\n`account_unlinked`: The account has no login and no card, so nobody could present a pass on it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"card_not_assigned","message":"A real Ta'ab card, but in stock, returned, lost or retired."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/passes":{"post":{"operationId":"issuePass","summary":"Issue a pass","description":"Puts a pass in a holder's wallet, keyed by your own reference. Either name one of your catalog products, or spell the pass out with type, validFrom, validUntil and uses. Sending the same externalRef again returns the original pass with 200 and replayed: true, so retries are safe. Selling a product that counts no uses to a holder already using the pass you sold them of it renews that pass instead: its validUntil moves on by the product's duration, and renewal says so.\n\nNeeds the scope `passes.issue`.","x-taab-scopes":["passes.issue"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"taabId":{"type":"string","minLength":1,"maxLength":40,"description":"The holder's Ta'ab ID, as returned by identity/resolve."},"externalRef":{"type":"string","minLength":1,"maxLength":120,"pattern":"^[A-Za-z0-9._:-]+$","description":"Your own reference for the pass, such as a ticket number. Letters, digits and . _ : - only. It is permanent and is your idempotency key for issuing."},"productId":{"description":"Issue from one of your catalog products (see GET /products). The product decides the pass's type, uses and length, so type, uses and validUntil can't be sent with it.","type":"string","minLength":1,"maxLength":80},"type":{"description":"What kind of pass this is. Defaults to transit. Use general when none of the others fits.","type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"validFrom":{"description":"When the pass starts being valid. Required unless productId is sent, where it defaults to now.","type":"string","format":"date-time"},"validUntil":{"description":"When the pass stops being valid. Required unless productId is sent.","type":"string","format":"date-time"},"uses":{"description":"How many times the pass can be redeemed, 1 to 100. Defaults to 1. null means unlimited within the validity window.","anyOf":[{"type":"integer","minimum":1,"maximum":100},{"type":"null"}]},"metadata":{"default":{},"type":"object","properties":{"label":{"description":"Headline shown on the pass.","type":"string","maxLength":120},"origin":{"description":"Where a trip starts.","type":"string","maxLength":120},"destination":{"description":"Where a trip ends.","type":"string","maxLength":120},"departureAt":{"description":"When a trip leaves (ISO 8601).","type":"string","format":"date-time"},"seat":{"description":"Seat or place.","type":"string","maxLength":20}},"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"description":"Display data for the pass in the Ta'ab app. Five keys are layout hints (label, origin, destination, departureAt, seat); you may add up to 20 keys of your own with string, number, boolean or null values."}},"required":["taabId","externalRef"]},"example":{"taabId":"TB-00000001234","externalRef":"TKT-8842-1","type":"transit","validFrom":"2026-10-14T12:00:00Z","validUntil":"2026-10-14T16:00:00Z","uses":1,"metadata":{"label":"Belize City → Orange Walk","origin":"Belize City","destination":"Orange Walk","departureAt":"2026-10-14T13:30:00.000Z","seat":"12"}}}}},"responses":{"201":{"description":"The pass was created, or renewed. 200 with replayed: true when the externalRef already existed.","content":{"application/json":{"schema":{"type":"object","properties":{"passId":{"type":"string","description":"Ta'ab's id for the pass."},"externalRef":{"type":"string","description":"Your reference for it."},"type":{"type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"productId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The catalog product it was issued from, or null."},"status":{"type":"string","enum":["active","expired","revoked"],"description":"What was done to the pass. A ticket that is used up, or past its window, still says active here; check remainingUses and validUntil."},"validFrom":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"validUntil":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"remainingUses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses left. null means unlimited within the window."},"dailyLimit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many times a day the pass can be used, the day being its institution's; past it a redemption answers denied_daily_limit. null means no limit."},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The display data you sent."},"renewal":{"anyOf":[{"type":"object","properties":{"externalRef":{"type":"string","description":"Your reference for this sale."},"addedDays":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many days it added to the pass's validUntil."}},"required":["externalRef","addedDays"],"additionalProperties":false,"description":"A sale that lengthened the holder's pass of this product instead of issuing a second one."},{"type":"null"}],"description":"Set when this sale renewed the pass the holder was already using: the pass is that one, with its own externalRef and the later validUntil. null for a new pass."},"replayed":{"type":"boolean","description":"true when this externalRef already existed and what it did the first time is returned."}},"required":["passId","externalRef","type","productId","status","validFrom","validUntil","remainingUses","dailyLimit","metadata","renewal","replayed"],"additionalProperties":false},"example":{"passId":"0b9c2f6e-6f0e-4c0f-9a6e-2f1f0b7c9d11","externalRef":"TKT-8842-1","type":"transit","productId":null,"status":"active","validFrom":"2026-10-14T12:00:00.000Z","validUntil":"2026-10-14T16:00:00.000Z","remainingUses":1,"dailyLimit":null,"metadata":{"label":"Belize City → Orange Walk","origin":"Belize City","destination":"Orange Walk","departureAt":"2026-10-14T13:30:00.000Z","seat":"12"},"renewal":null,"replayed":false}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`account_not_found`: No account has that Ta'ab ID.\n\n`product_not_found`: You have no product with that productId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"account_not_found","message":"No account has that Ta'ab ID."}}}},"409":{"description":"`account_inactive`: The account is suspended, closed or scrubbed.\n\n`account_unlinked`: The account has no login and no card, so nobody could present a pass on it.\n\n`already_held`: The holder already has a pass from this product that can't be renewed by this sale: it hasn't started yet, or someone else sold it.\n\n`concession_required`: The product is only for some concession profiles (students, seniors, staff), and the holder's roster places give them none of those.\n\n`conflict`: The externalRef is already used for a different account, the product is archived, or a renewal's days have begun so it can't be revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"account_inactive","message":"The account is suspended, closed or scrubbed."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}},"get":{"operationId":"listPasses","summary":"List your passes","description":"The passes you issued, newest first, a page at a time. Follow nextCursor until it is null.\n\nNeeds the scope `passes.issue` or `passes.redeem`. Read-only: writes nothing and never appears in the holder's history.","x-taab-scopes":["passes.issue","passes.redeem"],"parameters":[{"name":"taabId","in":"query","required":false,"description":"Only this holder's passes.","schema":{"type":"string","minLength":1,"maxLength":40}},{"name":"status","in":"query","required":false,"description":"Only passes with this stored status.","schema":{"type":"string","enum":["active","expired","revoked"]}},{"name":"limit","in":"query","required":false,"description":"How many to return, 1 to 200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"description":"The nextCursor from the previous page.","schema":{"type":"string","minLength":1,"maxLength":200}}],"responses":{"200":{"description":"A page of passes.","content":{"application/json":{"schema":{"type":"object","properties":{"passes":{"type":"array","items":{"type":"object","properties":{"passId":{"type":"string","description":"Ta'ab's id for the pass."},"externalRef":{"type":"string","description":"Your reference for it."},"type":{"type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"productId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The catalog product it was issued from, or null."},"status":{"type":"string","enum":["active","expired","revoked"],"description":"What was done to the pass. A ticket that is used up, or past its window, still says active here; check remainingUses and validUntil."},"validFrom":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"validUntil":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"remainingUses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses left. null means unlimited within the window."},"dailyLimit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many times a day the pass can be used, the day being its institution's; past it a redemption answers denied_daily_limit. null means no limit."},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The display data you sent."}},"required":["passId","externalRef","type","productId","status","validFrom","validUntil","remainingUses","dailyLimit","metadata"],"additionalProperties":false,"description":"One of your passes."},"description":"Newest first."},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Send as cursor for the next page. null on the last page."}},"required":["passes","nextCursor"],"additionalProperties":false},"example":{"passes":[{"passId":"0b9c2f6e-6f0e-4c0f-9a6e-2f1f0b7c9d11","externalRef":"TKT-8842-1","type":"transit","productId":null,"status":"active","validFrom":"2026-10-14T12:00:00.000Z","validUntil":"2026-10-14T16:00:00.000Z","remainingUses":1,"dailyLimit":null,"metadata":{"label":"Belize City → Orange Walk","origin":"Belize City","destination":"Orange Walk","departureAt":"2026-10-14T13:30:00.000Z","seat":"12"}}],"nextCursor":null}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`account_not_found`: No account has that Ta'ab ID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"account_not_found","message":"No account has that Ta'ab ID."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/passes/{externalRef}":{"get":{"operationId":"getPass","summary":"Read one pass","description":"One of your passes as it stands now.\n\nNeeds the scope `passes.issue` or `passes.redeem`. Read-only: writes nothing and never appears in the holder's history.","x-taab-scopes":["passes.issue","passes.redeem"],"parameters":[{"name":"externalRef","in":"path","required":true,"description":"Your reference for the pass.","schema":{"type":"string"}}],"responses":{"200":{"description":"The pass.","content":{"application/json":{"schema":{"type":"object","properties":{"passId":{"type":"string","description":"Ta'ab's id for the pass."},"externalRef":{"type":"string","description":"Your reference for it."},"type":{"type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"productId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The catalog product it was issued from, or null."},"status":{"type":"string","enum":["active","expired","revoked"],"description":"What was done to the pass. A ticket that is used up, or past its window, still says active here; check remainingUses and validUntil."},"validFrom":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"validUntil":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"remainingUses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses left. null means unlimited within the window."},"dailyLimit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many times a day the pass can be used, the day being its institution's; past it a redemption answers denied_daily_limit. null means no limit."},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The display data you sent."}},"required":["passId","externalRef","type","productId","status","validFrom","validUntil","remainingUses","dailyLimit","metadata"],"additionalProperties":false,"description":"One of your passes."},"example":{"passId":"0b9c2f6e-6f0e-4c0f-9a6e-2f1f0b7c9d11","externalRef":"TKT-8842-1","type":"transit","productId":null,"status":"active","validFrom":"2026-10-14T12:00:00.000Z","validUntil":"2026-10-14T16:00:00.000Z","remainingUses":1,"dailyLimit":null,"metadata":{"label":"Belize City → Orange Walk","origin":"Belize City","destination":"Orange Walk","departureAt":"2026-10-14T13:30:00.000Z","seat":"12"}}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`pass_not_found`: You have no pass with that externalRef.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"pass_not_found","message":"You have no pass with that externalRef."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/passes/{externalRef}/revoke":{"post":{"operationId":"revokePass","summary":"Revoke a pass","description":"Call it when a booking is cancelled, refunded or lapses. Revoking twice is harmless: the second call returns replayed: true. A renewal's reference takes that renewal's days back off the pass, which stays active, and is refused once those days have begun.\n\nNeeds the scope `passes.issue`.","x-taab-scopes":["passes.issue"],"parameters":[{"name":"externalRef","in":"path","required":true,"description":"Your reference for the pass.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","minLength":1,"maxLength":200,"description":"Why the pass is being revoked. Shown to Ta'ab admins."}},"required":["reason"]},"example":{"reason":"booking cancelled"}}}},"responses":{"200":{"description":"The pass, now revoked, or shorter by the renewal.","content":{"application/json":{"schema":{"type":"object","properties":{"passId":{"type":"string","description":"Ta'ab's id for the pass."},"externalRef":{"type":"string","description":"Your reference for it."},"type":{"type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"productId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The catalog product it was issued from, or null."},"status":{"type":"string","enum":["active","expired","revoked"],"description":"What was done to the pass. A ticket that is used up, or past its window, still says active here; check remainingUses and validUntil."},"validFrom":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"validUntil":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"remainingUses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses left. null means unlimited within the window."},"dailyLimit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many times a day the pass can be used, the day being its institution's; past it a redemption answers denied_daily_limit. null means no limit."},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The display data you sent."},"replayed":{"type":"boolean","description":"true when it was already revoked and is returned unchanged."}},"required":["passId","externalRef","type","productId","status","validFrom","validUntil","remainingUses","dailyLimit","metadata","replayed"],"additionalProperties":false},"example":{"passId":"0b9c2f6e-6f0e-4c0f-9a6e-2f1f0b7c9d11","externalRef":"TKT-8842-1","type":"transit","productId":null,"status":"revoked","validFrom":"2026-10-14T12:00:00.000Z","validUntil":"2026-10-14T16:00:00.000Z","remainingUses":1,"dailyLimit":null,"metadata":{"label":"Belize City → Orange Walk","origin":"Belize City","destination":"Orange Walk","departureAt":"2026-10-14T13:30:00.000Z","seat":"12"},"replayed":false}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`pass_not_found`: You have no pass with that externalRef.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"pass_not_found","message":"You have no pass with that externalRef."}}}},"409":{"description":"`conflict`: The externalRef is already used for a different account, the product is archived, or a renewal's days have begun so it can't be revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"conflict","message":"The externalRef is already used for a different account, the product is archived, or a renewal's days have begun so it can't be revoked."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/passes/verify":{"post":{"operationId":"verifyHolder","summary":"Who is presenting, and what can they use?","description":"Returns the holder and the passes of yours they could still use. A pass for a later trip is included: you pick the one that matches what is happening now, then redeem it. acceptedPasses lists what they could use with you without naming a pass: passes your institution accepts, such as a T-Pass, including one you sold from your own catalog. Never a ticket.\n\nNeeds the scope `passes.redeem`. Read-only: writes nothing and never appears in the holder's history.","x-taab-scopes":["passes.redeem"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"credential":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"nfc"},"uid":{"type":"string","minLength":1,"maxLength":64,"description":"The card's UID exactly as the reader reports it. Any spelling is accepted: 04A22B91, 04:a2:2b:91, 04 A2 2B 91."}},"required":["type","uid"]},{"type":"object","properties":{"type":{"type":"string","const":"qr"},"token":{"type":"string","minLength":1,"maxLength":500,"description":"The string decoded from the QR in the Ta'ab app. It rotates every 60 seconds; with no signal the app shows an offline code (o1.…) instead, good for 10 minutes, which Ta'ab takes too."}},"required":["type","token"]}],"description":"What the holder presented: a card tap or the Ta'ab app's QR. Send the raw value, never a hash."}},"required":["credential"]},"example":{"credential":{"type":"qr","token":"kZ04_mA2-n2Bp91qRsTuVwXyZ..."}}}}},"responses":{"200":{"description":"The holder and their usable passes.","content":{"application/json":{"schema":{"type":"object","properties":{"holder":{"type":"object","properties":{"taabId":{"type":"string","description":"The holder's permanent Ta'ab ID. Store it against your own user."},"accountStatus":{"type":"string","description":"Always active on a 200."},"credentialType":{"type":"string","enum":["nfc","qr"]},"card":{"anyOf":[{"type":"object","properties":{"cardNumber":{"type":"string","description":"The number printed on the card."},"status":{"type":"string","description":"Always assigned on a 200."}},"required":["cardNumber","status"],"additionalProperties":false},{"type":"null"}],"description":"The card behind an NFC tap. null for a QR."}},"required":["taabId","accountStatus","credentialType","card"],"additionalProperties":false,"description":"Who is presenting."},"passes":{"type":"array","items":{"type":"object","properties":{"passId":{"type":"string","description":"Ta'ab's id for the pass."},"externalRef":{"type":"string","description":"Your reference for it."},"type":{"type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"productId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The catalog product it was issued from, or null."},"status":{"type":"string","enum":["active","expired","revoked"],"description":"What was done to the pass. A ticket that is used up, or past its window, still says active here; check remainingUses and validUntil."},"validFrom":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"validUntil":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"remainingUses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses left. null means unlimited within the window."},"dailyLimit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many times a day the pass can be used, the day being its institution's; past it a redemption answers denied_daily_limit. null means no limit."},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The display data you sent."}},"required":["passId","externalRef","type","productId","status","validFrom","validUntil","remainingUses","dailyLimit","metadata"],"additionalProperties":false,"description":"One of your passes."},"description":"Your active passes for this holder that have not passed validUntil, soonest first."},"acceptedPasses":{"type":"array","items":{"type":"object","properties":{"passId":{"type":"string","description":"Ta'ab's id for the pass."},"type":{"type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"productId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The catalog product it was issued from, or null."},"name":{"type":"string","description":"What to call it on a screen: the product's name, or the kind of pass."},"validFrom":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When it starts being valid."},"validUntil":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When it stops being valid. For a pass that starts on first use and has not been used, the deadline for that first use (see windowDays)."},"remainingUses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses left. null means unlimited within the window."},"windowDays":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Set only while a pass that starts on first use has not been used: how many days it will run from its first redemption. null otherwise."},"dailyLimit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many times a day the pass can be used, the day being its institution's; past it a redemption answers denied_daily_limit. null means no limit."},"externalRef":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Your reference for it, when you issued it; null for a pass someone else issued."},"relation":{"type":"string","enum":["issued","accepted"],"description":"issued: your institution's pass, whoever sold it. accepted: another institution's pass that yours honours."},"renewable":{"type":"boolean","description":"true when selling its product again (POST /passes with its productId) renews this pass: you sold it, its product counts no uses, and it has started. false for a pass of that product you can't renew: selling it would answer already_held."}},"required":["passId","type","productId","name","validFrom","validUntil","remainingUses","windowDays","dailyLimit","externalRef","relation","renewable"],"additionalProperties":false,"description":"A pass your institution accepts that this holder could use without naming it: one from another institution, or one you sold from your own catalog."},"description":"Passes this holder could use with you right now without naming one: a T-Pass, a membership your institution accepts, or a pass you sold from your own catalog. Never a ticket. In the order Ta'ab would spend them. Redeem one by calling passes/redeem without externalRef."}},"required":["holder","passes","acceptedPasses"],"additionalProperties":false},"example":{"holder":{"taabId":"TB-00000001234","accountStatus":"active","credentialType":"qr","card":null},"passes":[{"passId":"0b9c2f6e-6f0e-4c0f-9a6e-2f1f0b7c9d11","externalRef":"TKT-8842-1","type":"transit","productId":null,"status":"active","validFrom":"2026-10-14T12:00:00.000Z","validUntil":"2026-10-14T16:00:00.000Z","remainingUses":1,"dailyLimit":null,"metadata":{"label":"Belize City → Orange Walk","origin":"Belize City","destination":"Orange Walk","departureAt":"2026-10-14T13:30:00.000Z","seat":"12"}}],"acceptedPasses":[{"passId":"7c1d4e9a-2b3f-4c5d-8e6f-0a1b2c3d4e5f","type":"transit","productId":"9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d","name":"T-Pass 30 days","validUntil":"2026-11-12T06:00:00.000Z","remainingUses":null,"windowDays":null,"dailyLimit":null,"validFrom":"2026-10-13T06:00:00.000Z","externalRef":"TPASS-2210","relation":"issued","renewable":true}]}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`credential_not_found`: No Ta'ab card has that UID, or the QR is not one Ta'ab issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"credential_not_found","message":"No Ta'ab card has that UID, or the QR is not one Ta'ab issued."}}}},"409":{"description":"`card_not_assigned`: A real Ta'ab card, but in stock, returned, lost or retired.\n\n`credential_stale`: The QR token has been superseded by a newer one. Ask the holder to refresh and scan again.\n\n`credential_revoked`: The QR credential was revoked (a lost phone).\n\n`account_inactive`: The account is suspended, closed or scrubbed.\n\n`account_unlinked`: The account has no login and no card, so nobody could present a pass on it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"card_not_assigned","message":"A real Ta'ab card, but in stock, returned, lost or retired."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/passes/redeem":{"post":{"operationId":"redeemPass","summary":"Redeem a pass","description":"Consumes one use of a pass for whoever is presenting the credential. Name the pass with externalRef, or leave it out and Ta'ab uses the holder's best pass of that type that your institution accepts. A denial is not an error: it is HTTP 200 with result: denied_…, and it is recorded in the holder's history. Reusing an idempotencyKey returns the original decision and consumes nothing more, so retry freely on a timeout.\n\nNeeds the scope `passes.redeem`.","x-taab-scopes":["passes.redeem"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"credential":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"nfc"},"uid":{"type":"string","minLength":1,"maxLength":64,"description":"The card's UID exactly as the reader reports it. Any spelling is accepted: 04A22B91, 04:a2:2b:91, 04 A2 2B 91."}},"required":["type","uid"]},{"type":"object","properties":{"type":{"type":"string","const":"qr"},"token":{"type":"string","minLength":1,"maxLength":500,"description":"The string decoded from the QR in the Ta'ab app. It rotates every 60 seconds; with no signal the app shows an offline code (o1.…) instead, good for 10 minutes, which Ta'ab takes too."}},"required":["type","token"]}],"description":"What the holder presented: a card tap or the Ta'ab app's QR. Send the raw value, never a hash."},"externalRef":{"description":"The pass to redeem, by your own reference. Leave it out to redeem whatever pass of `type` this holder has that your institution accepts, such as a T-Pass; a ticket (a pass you issued spelled out, not from a product) is only ever redeemed by its reference.","type":"string","minLength":1,"maxLength":120,"pattern":"^[A-Za-z0-9._:-]+$"},"type":{"description":"Only without externalRef: the kind of pass to look for. Defaults to transit.","type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"operatorRef":{"description":"Your own name or id for the operator doing the accepting, when you front several (a bus company). Recorded with the redemption and returned in GET /redemptions; Ta'ab's usage report groups by it.","type":"string","minLength":1,"maxLength":80},"idempotencyKey":{"type":"string","minLength":1,"maxLength":200,"description":"One key per real-world redemption. Reusing it returns the original decision and consumes nothing more."},"occurredAt":{"description":"When the tap happened, by your device's clock (ISO 8601).","type":"string","format":"date-time"},"offline":{"description":"true when your device decided this ride with no signal (from GET /passes/offline) and is uploading it now. The ride stands: Ta'ab records it as approved and says in offlineDenial what it would have answered at the time. occurredAt must be within the last 72 hours.","type":"boolean"}},"required":["credential","idempotencyKey","occurredAt"]},"example":{"credential":{"type":"nfc","uid":"04A22B9151C280"},"externalRef":"TKT-8842-1","idempotencyKey":"ticket-55120-checkin","occurredAt":"2026-10-14T13:22:05Z"}}}},"responses":{"200":{"description":"The decision: approved, or the reason for the denial.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","enum":["approved","denied_expired","denied_revoked","denied_insufficient_balance","denied_no_uses_remaining","denied_unknown_credential","denied_suspended","denied_wrong_institution","denied_not_holder","denied_recently_used","denied_daily_limit"],"description":"approved, or the reason for the denial. A denial is still HTTP 200."},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The denial in words, for a screen. null when approved."},"externalRef":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The reference you sent. null only when you sent none."},"passId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The pass the decision was about. null only when you sent no externalRef and the holder has no pass of that type at all."},"remainingUses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses left on the pass after this call."},"taabId":{"type":"string","description":"The presenter's Ta'ab ID. On denied_not_holder this is not the pass's owner."},"occurredAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"replayed":{"type":"boolean","description":"true when this idempotencyKey was seen before and the original decision is returned."},"offline":{"type":"boolean","description":"true when you sent offline: true. The ride was decided on your device and stands."},"offlineDenial":{"anyOf":[{"type":"string","enum":["approved","denied_expired","denied_revoked","denied_insufficient_balance","denied_no_uses_remaining","denied_unknown_credential","denied_suspended","denied_wrong_institution","denied_not_holder","denied_recently_used","denied_daily_limit"]},{"type":"null"}],"description":"Only on an offline redemption: what Ta'ab would have answered at the time, had it been asked (the result is still approved). null when it would have approved too."}},"required":["result","reason","externalRef","passId","remainingUses","taabId","occurredAt","replayed","offline","offlineDenial"],"additionalProperties":false},"example":{"result":"approved","reason":null,"externalRef":"TKT-8842-1","passId":"0b9c2f6e-6f0e-4c0f-9a6e-2f1f0b7c9d11","remainingUses":0,"taabId":"TB-00000001234","occurredAt":"2026-10-14T13:22:05.000Z","replayed":false,"offline":false,"offlineDenial":null}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`pass_not_found`: You have no pass with that externalRef.\n\n`credential_not_found`: No Ta'ab card has that UID, or the QR is not one Ta'ab issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"pass_not_found","message":"You have no pass with that externalRef."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/charges":{"post":{"operationId":"chargeBalance","summary":"Charge a holder's balance","description":"Takes amountCents from the balance the presenting holder keeps with your institution (a school canteen's), when they present their card or QR. The card is the consent. Before Ta'ab's payment licence, that balance is the only one you can charge, and it is spent only at your institution. A decline is not an error: it is HTTP 200 with result: declined and the reason, and nothing is taken. Reusing an idempotencyKey returns the original answer and takes nothing more.\n\nNeeds the scope `wallet.charge`.","x-taab-scopes":["wallet.charge"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"credential":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"nfc"},"uid":{"type":"string","minLength":1,"maxLength":64,"description":"The card's UID exactly as the reader reports it. Any spelling is accepted: 04A22B91, 04:a2:2b:91, 04 A2 2B 91."}},"required":["type","uid"]},{"type":"object","properties":{"type":{"type":"string","const":"qr"},"token":{"type":"string","minLength":1,"maxLength":500,"description":"The string decoded from the QR in the Ta'ab app. It rotates every 60 seconds; with no signal the app shows an offline code (o1.…) instead, good for 10 minutes, which Ta'ab takes too."}},"required":["type","token"]}],"description":"What the holder presented: a card tap or the Ta'ab app's QR. Send the raw value, never a hash."},"amountCents":{"type":"integer","minimum":1,"maximum":100000,"description":"What to take, in BZD cents."},"idempotencyKey":{"type":"string","minLength":1,"maxLength":200,"description":"One key per real-world charge. Reusing it returns the original answer and takes nothing more."},"reason":{"description":"What it was for, as the holder will read it on their statement (\"Lunch\").","type":"string","minLength":1,"maxLength":120}},"required":["credential","amountCents","idempotencyKey"]},"example":{"credential":{"type":"nfc","uid":"04A22B9151C280"},"amountCents":350,"idempotencyKey":"till-3-20261027-0042","reason":"Lunch"}}}},"responses":{"200":{"description":"Approved with the balance left, or declined with the reason.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","enum":["approved","declined"],"description":"A decline is still HTTP 200: render it."},"reason":{"anyOf":[{"type":"string","enum":["insufficient_balance","over_daily_limit","wrong_organisation","purse_frozen","account_inactive","credential_revoked","licence_required","not_merchant"]},{"type":"null"}],"description":"Why it was declined. null when approved. insufficient_balance also covers a holder with no balance kept by your organisation."},"taabId":{"type":"string","description":"The presenter's Ta'ab ID."},"amountCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"balanceAfterCents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"The holder's balance with your organisation after the charge. null when declined."},"chargeId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Ta'ab's id for the charge. null when declined."},"replayed":{"type":"boolean","description":"true when this idempotencyKey was seen before and the original answer is returned."}},"required":["result","reason","taabId","amountCents","balanceAfterCents","chargeId","replayed"],"additionalProperties":false},"example":{"result":"approved","reason":null,"taabId":"TB-00000001234","amountCents":350,"balanceAfterCents":1650,"chargeId":"9b2f6c1e-5d0a-4a52-9c1b-2f7a3e1d8c40","replayed":false}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`credential_not_found`: No Ta'ab card has that UID, or the QR is not one Ta'ab issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"credential_not_found","message":"No Ta'ab card has that UID, or the QR is not one Ta'ab issued."}}}},"409":{"description":"`credential_stale`: The QR token has been superseded by a newer one. Ask the holder to refresh and scan again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"credential_stale","message":"The QR token has been superseded by a newer one. Ask the holder to refresh and scan again."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/redemptions":{"get":{"operationId":"listRedemptions","summary":"List your redemptions","description":"Every redemption you made, approvals and denials, oldest first. To follow new ones, keep the last nextCursor and ask again with it.\n\nNeeds the scope `passes.redeem`. Read-only: writes nothing and never appears in the holder's history.","x-taab-scopes":["passes.redeem"],"parameters":[{"name":"since","in":"query","required":false,"description":"Only redemptions recorded at or after this time (ISO 8601).","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","required":false,"description":"How many to return, 1 to 200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"description":"The nextCursor from the previous page.","schema":{"type":"string","minLength":1,"maxLength":200}}],"responses":{"200":{"description":"A page of redemptions.","content":{"application/json":{"schema":{"type":"object","properties":{"redemptions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"result":{"type":"string","enum":["approved","denied_expired","denied_revoked","denied_insufficient_balance","denied_no_uses_remaining","denied_unknown_credential","denied_suspended","denied_wrong_institution","denied_not_holder","denied_recently_used","denied_daily_limit"]},"externalRef":{"anyOf":[{"type":"string"},{"type":"null"}]},"passId":{"anyOf":[{"type":"string"},{"type":"null"}]},"taabId":{"type":"string","description":"Who presented."},"occurredAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When the tap happened, by your clock."},"recordedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When Ta'ab recorded it."},"idempotencyKey":{"anyOf":[{"type":"string"},{"type":"null"}]},"operatorRef":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The operatorRef you sent with it, or null."},"offline":{"type":"boolean","description":"Decided on your device with no signal and uploaded later."},"offlineDenial":{"anyOf":[{"type":"string","enum":["approved","denied_expired","denied_revoked","denied_insufficient_balance","denied_no_uses_remaining","denied_unknown_credential","denied_suspended","denied_wrong_institution","denied_not_holder","denied_recently_used","denied_daily_limit"]},{"type":"null"}],"description":"For an offline redemption, what Ta'ab would have answered at the time; null otherwise."}},"required":["id","result","externalRef","passId","taabId","occurredAt","recordedAt","idempotencyKey","operatorRef","offline","offlineDenial"],"additionalProperties":false,"description":"One redemption you made, approved or denied."},"description":"Oldest first."},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Where to resume from. Returned even when you are caught up, so you can poll with it."},"hasMore":{"type":"boolean","description":"true when another page is ready now."}},"required":["redemptions","nextCursor","hasMore"],"additionalProperties":false},"example":{"redemptions":[{"id":"5d16c08d-f833-4518-a93d-3723d75aa103","result":"approved","externalRef":"TKT-8842-1","passId":"0b9c2f6e-6f0e-4c0f-9a6e-2f1f0b7c9d11","taabId":"TB-00000001234","occurredAt":"2026-10-14T13:22:05.000Z","recordedAt":"2026-10-14T13:22:06.412Z","idempotencyKey":"ticket-55120-checkin","operatorRef":null,"offline":false,"offlineDenial":null}],"nextCursor":"MjAyNi0xMC0xNFQxMzoyMjowNi40MTJafDVkMTZjMDhk","hasMore":false}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/offline/passes":{"get":{"operationId":"getOfflineList","summary":"Who may board with no signal","description":"For a scanner that loses signal: every holder with a pass your institution honours at some point in the next 24 hours, with the hashes of their cards' UIDs and their Ta'ab ID (what an offline QR code carries). Fetch it while online, a page at a time, and refresh it often. With no signal, accept a card whose UID hash is listed, or an offline QR code whose signature checks out (GET /offline/key) and whose Ta'ab ID is listed; apply your own rule against a second tap. Then upload each ride with passes/redeem and offline: true.\n\nNeeds the scope `passes.redeem`. Read-only: writes nothing and never appears in the holder's history.","x-taab-scopes":["passes.redeem"],"parameters":[{"name":"type","in":"query","required":false,"description":"The kind of pass to list. Defaults to transit.","schema":{"default":"transit","type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]}},{"name":"cursor","in":"query","required":false,"description":"The nextCursor from the previous page.","schema":{"type":"string","minLength":1,"maxLength":200}}],"responses":{"200":{"description":"A page of the list.","content":{"application/json":{"schema":{"type":"object","properties":{"generatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"honourUntil":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"Accept from this list until then, 24 hours on. Past it, take no offline rides: fetch a new list first."},"holders":{"type":"array","items":{"type":"object","properties":{"taabId":{"type":"string","description":"What an offline QR code carries."},"cardUidHashes":{"type":"array","items":{"type":"string"},"description":"sha256, in lowercase hex, of each of the holder's cards' UIDs, normalised: hex digits only, uppercase (04A22B9151C280). Hash the UID you read the same way and look it up."},"passes":{"type":"array","items":{"type":"object","properties":{"passId":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The product's name, for your screen."},"validFrom":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"validUntil":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"For a pass that starts on first use and has not, the deadline for that first use."},"remainingUses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses left when the list was made; null for unlimited."},"windowDays":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["passId","name","validFrom","validUntil","remainingUses","windowDays"],"additionalProperties":false},"description":"The holder's passes your institution honours within the list's window."}},"required":["taabId","cardUidHashes","passes"],"additionalProperties":false},"description":"A page of holders, by no particular order."},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Send as cursor for the next page. null on the last page."}},"required":["generatedAt","honourUntil","holders","nextCursor"],"additionalProperties":false},"example":{"generatedAt":"2026-10-16T12:00:00.000Z","honourUntil":"2026-10-17T12:00:00.000Z","holders":[{"taabId":"TB-00000001234","cardUidHashes":["5f2b0c6e9d1a4b7c8e3f2a1d0c9b8a7e6f5d4c3b2a1908f7e6d5c4b3a2918070"],"passes":[{"passId":"7c1d4e9a-2b3f-4c5d-8e6f-0a1b2c3d4e5f","name":"T-Pass 30 days","validFrom":"2026-10-13T06:00:00.000Z","validUntil":"2026-11-12T06:00:00.000Z","remainingUses":null,"windowDays":null}]}],"nextCursor":null}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/offline/key":{"get":{"operationId":"getOfflineKey","summary":"The key that signs offline QR codes","description":"With no signal the Ta'ab app shows an offline code instead of its rotating QR: o1.{kid}.{taabId}.{window}.{signature}. The signature is Ed25519 over everything before the last dot, and window is the 10-minute slot since the Unix epoch (floor(unix seconds / 600)) the code is good for; accept the slot either side of your clock's. Online, send the code to Ta'ab as a qr credential like any other.\n\nNeeds the scope `passes.redeem`. Read-only: writes nothing and never appears in the holder's history.","x-taab-scopes":["passes.redeem"],"parameters":[],"responses":{"200":{"description":"The public key.","content":{"application/json":{"schema":{"type":"object","properties":{"kid":{"type":"string","description":"The key's id; an offline code carries it as its second field."},"algorithm":{"type":"string","const":"Ed25519"},"publicKey":{"type":"string","description":"The raw 32-byte Ed25519 public key, base64url."},"windowSeconds":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How long one offline code is good for."}},"required":["kid","algorithm","publicKey","windowSeconds"],"additionalProperties":false},"example":{"kid":"3fa9c2d1","algorithm":"Ed25519","publicKey":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo","windowSeconds":600}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`offline_unavailable`: Offline codes are not set up on this Ta'ab, so there is no key to check them with.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"offline_unavailable","message":"Offline codes are not set up on this Ta'ab, so there is no key to check them with."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/products":{"get":{"operationId":"listProducts","summary":"List your catalog","description":"The active products of your institution. Pass a productId to POST /passes to issue from one.\n\nNeeds the scope `passes.issue`. Read-only: writes nothing and never appears in the holder's history.","x-taab-scopes":["passes.issue"],"parameters":[],"responses":{"200":{"description":"Your catalog.","content":{"application/json":{"schema":{"type":"object","properties":{"products":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"tier":{"anyOf":[{"type":"string"},{"type":"null"}]},"priceCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The price shown at the counter, in BZD cents. Ta'ab does not charge it."},"durationDays":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How long a pass issued from this product is valid."},"startsOn":{"type":"string","enum":["issue","first_use"],"description":"When those days start counting: issue (the day the pass is issued) or first_use (its first redemption; until then it must be used within a year)."},"uses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses on the pass. null means unlimited within its window."},"dailyLimit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses a day on a pass issued from it, the day being your institution's. null means no limit."},"concessions":{"anyOf":[{"type":"array","items":{"type":"string","enum":["student","senior","staff"]}},{"type":"null"}],"description":"Who it may be issued to: a holder whose roster places give them one of these concession profiles. null means anyone. Issuing it to anyone else answers concession_required."}},"required":["productId","name","type","tier","priceCents","durationDays","startsOn","uses","dailyLimit","concessions"],"additionalProperties":false,"description":"Something in your catalog a pass can be issued from."}}},"required":["products"],"additionalProperties":false},"example":{"products":[{"productId":"3f1c7a52-0d7e-4a51-9a3b-6f0b2f7d8e21","name":"Monthly pass","type":"general","tier":null,"priceCents":4500,"durationDays":30,"startsOn":"issue","uses":null,"dailyLimit":null,"concessions":null}]}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/cards/activate":{"post":{"operationId":"activateCard","summary":"Activate a card for a walk-in rider","description":"Turns one of your institution's stock cards into a working Ta'ab card for a rider with no Ta'ab login, and perhaps no phone. You get the card's Ta'ab ID, to issue passes to as usual, and a claim code to print on the rider's slip: entered in the Ta'ab app, it puts the card and its passes on their own account. Activating a card that is already yours answers again with its Ta'ab ID and no code. A card lost before anyone claims it is gone, with its passes.\n\nNeeds the scope `cards.activate`.","x-taab-scopes":["cards.activate"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"uid":{"type":"string","minLength":1,"maxLength":64,"description":"The card's UID exactly as the reader reports it. Any spelling is accepted: 04A22B91, 04:a2:2b:91, 04 A2 2B 91."},"newClaimCode":{"description":"true to replace the claim code of a card you already activated and nobody has claimed: for when the first answer never reached you. The old code stops working.","type":"boolean"}},"required":["uid"]},"example":{"uid":"04:a2:2b:91:51:c2:80"}}}},"responses":{"201":{"description":"The card works now. 200 with replayed: true, and claimCode null unless you sent newClaimCode, when it was already activated.","content":{"application/json":{"schema":{"type":"object","properties":{"taabId":{"type":"string","description":"The card's account. Issue passes to it like any other, then the rider boards by tapping the card."},"cardNumber":{"type":"string","description":"The number printed on the card."},"claimCode":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Print it on the rider's slip: entered in the Ta'ab app or at /activate, it puts the card and its passes on their own account. Shown only in this answer; null when the card was already activated (unless you asked for newClaimCode)."},"replayed":{"type":"boolean","description":"true when the card was already activated: by this call earlier, or by someone else at your institution."}},"required":["taabId","cardNumber","claimCode","replayed"],"additionalProperties":false},"example":{"taabId":"TB-00000005521","cardNumber":"C-0107","claimCode":"TAAB-7K3QM-2XAP9","replayed":false}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`credential_not_found`: No Ta'ab card has that UID, or the QR is not one Ta'ab issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"credential_not_found","message":"No Ta'ab card has that UID, or the QR is not one Ta'ab issued."}}}},"409":{"description":"`conflict`: The externalRef is already used for a different account, the product is archived, or a renewal's days have begun so it can't be revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"conflict","message":"The externalRef is already used for a different account, the product is archived, or a renewal's days have begun so it can't be revoked."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/links":{"post":{"operationId":"createLink","summary":"Link a holder's account to yours","description":"Two ways. With the card or Ta'ab app QR the holder is presenting, send it as credential: presenting it is their consent, so the link is approved at once and the answer carries their Ta'ab ID. Without one, the holder approves in Ta'ab and you read their Ta'ab ID back: send them to url (with a returnUrl, Ta'ab sends them back to you when they answer), or show them code to type at /connect in Ta'ab, then poll GET /links/{linkId}; they must answer within 10 minutes. Either way the holder sees your app in Ta'ab and can remove it, and you may read the passes they hold that you issue or honour (GET /accounts/{taabId}/passes). Keep the linkId: if a card you linked is claimed into another Ta'ab account, the link follows it.\n\nNeeds the scope `connections.link`.","x-taab-scopes":["connections.link"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"credential":{"description":"The card or Ta'ab app QR the holder is presenting to you. Presenting it is their consent: the link is approved at once. Leave it out to ask them to approve in Ta'ab instead.","oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"nfc"},"uid":{"type":"string","minLength":1,"maxLength":64,"description":"The card's UID exactly as the reader reports it. Any spelling is accepted: 04A22B91, 04:a2:2b:91, 04 A2 2B 91."}},"required":["type","uid"]},{"type":"object","properties":{"type":{"type":"string","const":"qr"},"token":{"type":"string","minLength":1,"maxLength":500,"description":"The string decoded from the QR in the Ta'ab app. It rotates every 60 seconds; with no signal the app shows an offline code (o1.…) instead, good for 10 minutes, which Ta'ab takes too."}},"required":["type","token"]}]},"returnUrl":{"description":"Where to send the holder after they answer: one of your client's registered return URLs, exactly. Ta'ab adds link and status to its query. Leave it out for the code flow, and when you send a credential.","type":"string","maxLength":500},"reference":{"description":"Your own id for this attempt (a user id, a session). Echoed back.","type":"string","minLength":1,"maxLength":200}}},"example":{"credential":{"type":"nfc","uid":"04:a2:2b:91:51:c2:80"},"reference":"user-5521"}}}},"responses":{"201":{"description":"The link. With a credential it is already approved; without one, code and url are shown only here.","content":{"application/json":{"schema":{"type":"object","properties":{"linkId":{"type":"string","description":"Keep it: GET /links/{linkId} answers with the holder's Ta'ab ID as long as the link stands, including after a card you linked is claimed into another Ta'ab account."},"status":{"type":"string","enum":["pending","approved"],"description":"pending until the holder answers in Ta'ab; approved at once when you sent a credential."},"taabId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The holder's Ta'ab ID. Set when you sent a credential; null while pending."},"card":{"anyOf":[{"type":"object","properties":{"cardNumber":{"type":"string","description":"The number printed on the card."},"status":{"type":"string"}},"required":["cardNumber","status"],"additionalProperties":false},{"type":"null"}],"description":"The card that was tapped, when the credential was a card. null otherwise."},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Show it to the holder if they are not on a device that can open url (a kiosk, a POS). They type it at /connect in Ta'ab. Shown only in this answer. null when you sent a credential."},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Send the holder here (a redirect, a link, a QR). They sign in to Ta'ab and approve or decline. null when you sent a credential."},"expiresAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"The holder must answer before this, 10 minutes from now. For a link made with a credential, when it was made."}},"required":["linkId","status","taabId","card","code","url","expiresAt"],"additionalProperties":false},"example":{"linkId":"3f2c8a1e-9b4d-4e6f-a1c2-7d8e9f0a1b2c","status":"approved","taabId":"TB-00000001234","card":{"cardNumber":"C-0041","status":"assigned"},"code":null,"url":null,"expiresAt":"2026-10-14T13:00:00.000Z"}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`credential_not_found`: No Ta'ab card has that UID, or the QR is not one Ta'ab issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"credential_not_found","message":"No Ta'ab card has that UID, or the QR is not one Ta'ab issued."}}}},"409":{"description":"`card_not_assigned`: A real Ta'ab card, but in stock, returned, lost or retired.\n\n`credential_stale`: The QR token has been superseded by a newer one. Ask the holder to refresh and scan again.\n\n`credential_revoked`: The QR credential was revoked (a lost phone).\n\n`account_inactive`: The account is suspended, closed or scrubbed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"card_not_assigned","message":"A real Ta'ab card, but in stock, returned, lost or retired."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/accounts/{taabId}/passes":{"get":{"operationId":"listHolderPasses","summary":"Read the passes a linked holder holds","description":"For a holder linked to you (POST /links) or on your institution's list: the live passes your institution issued or honours, whoever sold them. This is what a page showing someone their T-Pass reads. relation says whose each pass is, and renewable whether selling its product again renews it. Never a pass you don't honour.\n\nNeeds the scope `passes.issue` or `passes.redeem`. Read-only: writes nothing and never appears in the holder's history.","x-taab-scopes":["passes.issue","passes.redeem"],"parameters":[{"name":"taabId","in":"path","required":true,"description":"The holder's Ta'ab ID, from POST /links or GET /links/{linkId}.","schema":{"type":"string"}}],"responses":{"200":{"description":"The holder's passes that are good with you.","content":{"application/json":{"schema":{"type":"object","properties":{"taabId":{"type":"string"},"passes":{"type":"array","items":{"type":"object","properties":{"passId":{"type":"string","description":"Ta'ab's id for the pass."},"type":{"type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"productId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The catalog product it was issued from, or null."},"name":{"type":"string","description":"What to call it on a screen: the product's name, or the kind of pass."},"validFrom":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When it starts being valid."},"validUntil":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When it stops being valid. For a pass that starts on first use and has not been used, the deadline for that first use (see windowDays)."},"remainingUses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses left. null means unlimited within the window."},"windowDays":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Set only while a pass that starts on first use has not been used: how many days it will run from its first redemption. null otherwise."},"dailyLimit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many times a day the pass can be used, the day being its institution's; past it a redemption answers denied_daily_limit. null means no limit."},"externalRef":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Your reference for it, when you issued it; null for a pass someone else issued."},"relation":{"type":"string","enum":["issued","accepted"],"description":"issued: your institution's pass, whoever sold it. accepted: another institution's pass that yours honours."},"renewable":{"type":"boolean","description":"true when selling its product again (POST /passes with its productId) renews this pass: you sold it, its product counts no uses, and it has started. false for a pass of that product you can't renew: selling it would answer already_held."}},"required":["passId","type","productId","name","validFrom","validUntil","remainingUses","windowDays","dailyLimit","externalRef","relation","renewable"],"additionalProperties":false,"description":"A pass your institution accepts that this holder could use without naming it: one from another institution, or one you sold from your own catalog."},"description":"Live passes your institution issued or honours, whoever sold them: those usable now first, in the order Ta'ab would spend them, then those that start later. Never another institution's pass you don't honour."}},"required":["taabId","passes"],"additionalProperties":false,"description":"What a holder linked to you holds that is good with you."},"example":{"taabId":"TB-00000001234","passes":[{"passId":"7c1d4e9a-2b3f-4c5d-8e6f-0a1b2c3d4e5f","type":"transit","productId":"9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d","name":"T-Pass 30 days","validUntil":"2026-11-12T06:00:00.000Z","remainingUses":null,"windowDays":null,"dailyLimit":null,"validFrom":"2026-10-13T06:00:00.000Z","externalRef":"TPASS-2210","relation":"issued","renewable":true}]}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`account_not_found`: No account has that Ta'ab ID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"account_not_found","message":"No account has that Ta'ab ID."}}}},"409":{"description":"`not_connected`: The holder hasn't linked their account to you (POST /links) and isn't on your institution's list, so their passes aren't yours to read.\n\n`account_inactive`: The account is suspended, closed or scrubbed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"not_connected","message":"The holder hasn't linked their account to you (POST /links) and isn't on your institution's list, so their passes aren't yours to read."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}},"/links/{linkId}":{"get":{"operationId":"getLink","summary":"Read a link's answer","description":"pending until the holder answers. approved carries their Ta'ab ID; store it against your user. If they later unlink your app in Ta'ab, the link reads revoked and taabId is null.\n\nNeeds the scope `connections.link`. Read-only: writes nothing and never appears in the holder's history.","x-taab-scopes":["connections.link"],"parameters":[{"name":"linkId","in":"path","required":true,"description":"From POST /links.","schema":{"type":"string"}}],"responses":{"200":{"description":"The link and, once approved, the holder's Ta'ab ID.","content":{"application/json":{"schema":{"type":"object","properties":{"linkId":{"type":"string"},"status":{"type":"string","enum":["pending","approved","declined","expired","revoked"],"description":"pending until the holder answers; expired if they never did; revoked when they approved and later unlinked your app in Ta'ab."},"taabId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The holder's Ta'ab ID. Set only while status is approved."},"reference":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The reference you sent, echoed back."},"expiresAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"decidedAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}],"description":"When the holder approved or declined."}},"required":["linkId","status","taabId","reference","expiresAt","decidedAt"],"additionalProperties":false,"description":"A link request and, once approved, who it linked."},"example":{"linkId":"3f2c8a1e-9b4d-4e6f-a1c2-7d8e9f0a1b2c","status":"approved","taabId":"TB-00000001234","reference":"user-5521","expiresAt":"2026-10-14T13:10:00.000Z","decidedAt":"2026-10-14T13:02:41.000Z"}}}},"400":{"description":"`invalid_input`: The body, query or path is malformed, or a uid isn't a card UID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_input","message":"The body, query or path is malformed, or a uid isn't a card UID."}}}},"401":{"description":"`unauthorized`: Missing, wrong or disabled client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthorized","message":"Missing, wrong or disabled client credentials."}}}},"403":{"description":"`forbidden`: Valid client, but it lacks the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"forbidden","message":"Valid client, but it lacks the scope this route needs."}}}},"404":{"description":"`link_not_found`: You have no link request with that linkId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"link_not_found","message":"You have no link request with that linkId."}}}},"429":{"description":"`rate_limited`: More than 600 requests in a minute. Wait for the seconds in the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"More than 600 requests in a minute. Wait for the seconds in the Retry-After header."}}}}}}}},"components":{"securitySchemes":{"clientCredentials":{"type":"http","scheme":"basic","description":"Authorization: Basic base64(clientId:secret). Keep the secret on your backend."}},"schemas":{"Error":{"type":"object","properties":{"code":{"type":"string","enum":["invalid_input","unauthorized","forbidden","credential_not_found","account_not_found","pass_not_found","product_not_found","link_not_found","offline_unavailable","card_not_assigned","credential_stale","credential_revoked","account_inactive","account_unlinked","not_connected","concession_required","already_held","conflict","rate_limited"],"description":"Stable, machine-readable. Branch on this."},"message":{"type":"string","description":"For a person. May be reworded; do not parse it."}},"required":["code","message"],"additionalProperties":false,"description":"Every non-2xx answer has this shape."},"Holder":{"type":"object","properties":{"taabId":{"type":"string","description":"The holder's permanent Ta'ab ID. Store it against your own user."},"accountStatus":{"type":"string","description":"Always active on a 200."},"credentialType":{"type":"string","enum":["nfc","qr"]},"card":{"anyOf":[{"type":"object","properties":{"cardNumber":{"type":"string","description":"The number printed on the card."},"status":{"type":"string","description":"Always assigned on a 200."}},"required":["cardNumber","status"],"additionalProperties":false},{"type":"null"}],"description":"The card behind an NFC tap. null for a QR."}},"required":["taabId","accountStatus","credentialType","card"],"additionalProperties":false,"description":"Who is presenting."},"Pass":{"type":"object","properties":{"passId":{"type":"string","description":"Ta'ab's id for the pass."},"externalRef":{"type":"string","description":"Your reference for it."},"type":{"type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"productId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The catalog product it was issued from, or null."},"status":{"type":"string","enum":["active","expired","revoked"],"description":"What was done to the pass. A ticket that is used up, or past its window, still says active here; check remainingUses and validUntil."},"validFrom":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"validUntil":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"remainingUses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses left. null means unlimited within the window."},"dailyLimit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many times a day the pass can be used, the day being its institution's; past it a redemption answers denied_daily_limit. null means no limit."},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The display data you sent."}},"required":["passId","externalRef","type","productId","status","validFrom","validUntil","remainingUses","dailyLimit","metadata"],"additionalProperties":false,"description":"One of your passes."},"Redemption":{"type":"object","properties":{"id":{"type":"string"},"result":{"type":"string","enum":["approved","denied_expired","denied_revoked","denied_insufficient_balance","denied_no_uses_remaining","denied_unknown_credential","denied_suspended","denied_wrong_institution","denied_not_holder","denied_recently_used","denied_daily_limit"]},"externalRef":{"anyOf":[{"type":"string"},{"type":"null"}]},"passId":{"anyOf":[{"type":"string"},{"type":"null"}]},"taabId":{"type":"string","description":"Who presented."},"occurredAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When the tap happened, by your clock."},"recordedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When Ta'ab recorded it."},"idempotencyKey":{"anyOf":[{"type":"string"},{"type":"null"}]},"operatorRef":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The operatorRef you sent with it, or null."},"offline":{"type":"boolean","description":"Decided on your device with no signal and uploaded later."},"offlineDenial":{"anyOf":[{"type":"string","enum":["approved","denied_expired","denied_revoked","denied_insufficient_balance","denied_no_uses_remaining","denied_unknown_credential","denied_suspended","denied_wrong_institution","denied_not_holder","denied_recently_used","denied_daily_limit"]},{"type":"null"}],"description":"For an offline redemption, what Ta'ab would have answered at the time; null otherwise."}},"required":["id","result","externalRef","passId","taabId","occurredAt","recordedAt","idempotencyKey","operatorRef","offline","offlineDenial"],"additionalProperties":false,"description":"One redemption you made, approved or denied."},"Product":{"type":"object","properties":{"productId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["transit","meal","library","attendance","event","staff","visitor","general"]},"tier":{"anyOf":[{"type":"string"},{"type":"null"}]},"priceCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The price shown at the counter, in BZD cents. Ta'ab does not charge it."},"durationDays":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How long a pass issued from this product is valid."},"startsOn":{"type":"string","enum":["issue","first_use"],"description":"When those days start counting: issue (the day the pass is issued) or first_use (its first redemption; until then it must be used within a year)."},"uses":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses on the pass. null means unlimited within its window."},"dailyLimit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Uses a day on a pass issued from it, the day being your institution's. null means no limit."},"concessions":{"anyOf":[{"type":"array","items":{"type":"string","enum":["student","senior","staff"]}},{"type":"null"}],"description":"Who it may be issued to: a holder whose roster places give them one of these concession profiles. null means anyone. Issuing it to anyone else answers concession_required."}},"required":["productId","name","type","tier","priceCents","durationDays","startsOn","uses","dailyLimit","concessions"],"additionalProperties":false,"description":"Something in your catalog a pass can be issued from."}}}}