The GoTab Loyalty API is an events-driven API to be consumed by a third party loyalty program.
GoTab Loyalty API Dependencies:

Event Types:
INQUIRE:
The inquire event type will have a payload that includes the event_type: INQUIRE. It will also include the lookup value the customer entered. This data may be a phone number, an email, or a customer loyalty number depending on how the lookup_type value has been configured for your specific integration. If there are any items currently in the customers cart that are unpaid for it will also include that.
Here is an example INQUIRE event type request:
{
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "example_auth_header_123",
"x-api-key": "null"
},
"json": true,
"body": {
"tab_data": {
"tab_id": "14165",
"tab_uuid": "O2oFAC7fXeYNEWmmOBFZr_4S",
"location_id": "1019",
"name": "Austin Michael",
"spots": null,
"status": "PENDING",
"total": 1166,
"subtotal": 1100,
"tax": 66,
"balance_due": 1166,
"created": "2025-04-01T11:00:53.247Z",
"orders": [],
"items": [],
"adjustments": [],
"payments": [],
"customers": {},
"tab_metadata": null
},
"event_type": "INQUIRE",
"lookup_value": "+16082139090",
"location_id": "1019"
}
}
In order to indicate that a customer was found in your system please respond with a 200 status code. It is expected that even when there are no offers or points currently avaialble for a customer we will get a 200 status code with this JSON in the response:
{
"loyalty_points": [],
"offers": []
}
In the case where the customer is eligible for offers or points here are a few examples of what your JSON responses may look like:
Offers and Points
{
"loyalty_points": [
{
"type_display_name": "Loyalty Points",
"type": "points",
"total": 100,
"available": 100,
"value": 100,
"conversion_rate": 1
}
],
"offers": [
{
"name": "Offer Program Name",
"offers": [
{
"offer_id": "12344",
"name": "Free Drink",
"description": "This is good for any free drink",
"amount": 5,
"type": "tab_discount",
"exclusive_offer": false,
"group_exclusive_offer": false,
"auto_apply": false,
"allow_partial_use": false
}
]
}
]
}
Just Points
{
"loyalty_points": [
{
"type_display_name": "Loyalty Points",
"type": "points",
"total": 100,
"available": 100,
"value": 100,
"conversion_rate": 1
}
],
"offers": []
}
Just Offers
{
"loyalty_points": [ ],
"offers": [
{
"name": "Offer Program Name",
"offers": [
{
"offer_id": "12344",
"name": "Free Drink",
"description": "This is good for any free drink",
"amount": 5,
"type": "tab_discount",
"exclusive_offer": false,
"group_exclusive_offer": false,
"auto_apply": false,
"allow_partial_use": false
}
]
}
]
}
export const LoyaltyOffersSchema = z.object({
loyalty_points: z.array(LoyaltyPointSchema),
offers: z.array(OfferGroupSchema),
});
const LoyaltyPointSchema = z.object({
type_display_name: z.string(),
type: z.string(),
total: z.number().positive({ message: 'Point total must be greater than 0' }),
available: z.number(),
value: z.number().positive({ message: 'Point value must be greater than 0' }),
conversion_rate: z.number().positive({ message: 'Point conversion rate must be greater than 0' }),
});
const OfferGroupSchema = z.object({
name: z.string(),
offers: z.array(OfferSchema),
});
const OfferSchema = z.object({
offer_id: z.string(),
name: z.string(),
description: z.string(),
amount: z.number().positive({ message: 'Offer amount must be greater than 0' }),
type: z.string(),
target_id: z.string().optional(),
expiration_date: z.string().optional(),
exclusive_offer: z.boolean(),
group_exclusive_offer: z.boolean(),
auto_apply: z.boolean(),
allow_partial_use: z.boolean(),
});
In order to indicate a customer was not found in your system please respond with a status code 404 and a JSON response that looks like the following. Please note that the message is up to you. If you want to indicate that the customer should take some specific action please do so, but keep it succinct. Mainly, just be aware that any message you include will end up as an alert in our UI.
{
"message": "A membership was not found for that phone number."
}
For any other error please response with a 400 status code and an appropriate message.
REDEEM:
The REDEEM event type is an event that will be triggered each time a customer selects an offer or set of offers to be applied to their tab. The request will include the offer_id's that were supplied by your system in the response to the INQUIRE event. The response to the REDEEM event will indicate which selected offers are valid and should be applied as discounts to the customers tab, and which offers may no longer be valid (something may have changed betweetn the INQUIRE and REDEEM events).
Two things to note here:
- The data stucture includes a loyalty_points property, which is not currently supported by our Loyalty API. However, it is included here (and throughout these docs) because it will be implemented soon and we would like to avoid our integration partners having to make code adjustments on their end at that time.
- The rejected_offers objects allow you to include a rejected_reason property. While not required, this will serve as a way to indicate to the customer as well as an operator why an offer may not be functioning as expcted. So, please please as specific as possible here. Something like "offer has already been applied" or "offer cannot be applied after 7pm."
Here's an example REDEEM event request:
{
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "",
"x-api-key": "null"
},
"json": true,
"body": {
"selected_offers": ["1234", "5678"],
"tab_data": {}, // same structure as other events
"event_type": "REDEEM",
"location_id": "1019"
}
}
GoTab expects back a 200 status code with the below data structure as a response:
{
"loyalty_points": [],
"offers": {
"rejected_offers": [
{
"offer_id": "90909",
"name": "Three bucks off",
"description": "This is a three dollars off offer.",
"amount": 3,
"type": "tab_discount",
"exclusive_offer": false,
"group_exclusive_offer": false,
"auto_apply": false,
"allow_partial_use": false,
"rejected_reason": "Offer already redeemed."
}
],
"valid_offers": [
{
"offer_id": "12344",
"name": "Free Drink",
"description": "This is a free drink offer (five dollars).",
"amount": 5,
"type": "tab_discount",
"exclusive_offer": false,
"group_exclusive_offer": false,
"auto_apply": false,
"allow_partial_use": false
},
{
"offer_id": "56789",
"name": "Ten bucks off",
"description": "This is a ten dollars off offer.",
"amount": 10,
"type": "tab_discount",
"exclusive_offer": false,
"group_exclusive_offer": false,
"auto_apply": false,
"allow_partial_use": false
}
]
}
}
const offerBaseSchema = z.object({
offer_id: z.string(),
name: z.string(),
description: z.string(),
amount: z.number(),
type: z.literal("tab_discount"),
exclusive_offer: z.boolean(),
group_exclusive_offer: z.boolean(),
auto_apply: z.boolean(),
allow_partial_use: z.boolean()
});
const rejectedOfferSchema = offerBaseSchema.extend({
rejected_reason: z.string()
});
const validOfferSchema = offerBaseSchema;
const schema = z.object({
loyalty_points: z.array(z.any()),
offers: z.object({
rejected_offers: z.array(rejectedOfferSchema),
valid_offers: z.array(validOfferSchema)
})
});
ACCRUAL:
The ACCRUAL event type will be an asynchronous request that will be triggered for each tab at a location as they are closed and updated in GoTab. It will basically act as an export of the tab's sales data and will include the data of the customers associated with the tab.
We intentionally send an ACCRUAL event type request for every closed tab. This may include data for customers who are not currently using your loyalty program. The idea here is that you could still store the data and in the event a customer signs up for the loyalty program after having been at the location many times prior they may get credit for those previous visits. This would depend on the limitations of your program and whether the operator configured it to behave that way.
Here's an example request for an ACCRUAL event type request
{
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "example_auth_header_123",
"x-api-key": "null"
},
"json": true,
"body": {
"tab_data": {
"tab_id": "9200",
"tab_uuid": "tOp_3qizc55ojTehKtoGGKZc",
"location_id": "100800",
"name": "Test User",
"spots": ["109104"],
"status": "CLOSED",
"total": 1619,
"subtotal": 1295,
"tax": 130,
"balance_due": 0,
"created": "2025-02-17T13:10:51.759Z",
"orders": [
{
"tax": 130,
"name": "Test User",
"notes": null,
"total": 1619,
"placed": "2025-02-17T13:10:59.358228-05:00",
"status": "SCHEDULED",
"spot_id": 109104,
"user_id": null,
"zone_id": 16866,
"order_id": 72970599,
"subtotal": 1295,
"scheduled": "2025-02-17T13:10:59.358228-05:00",
"spot_name": "Bar 101",
"x_spot_id": null,
"x_zone_id": null,
"zone_name": "Bar",
"zone_type": null,
"address_id": null,
"dispatched": null,
"display_id": null,
"order_uuid": "or_WjMDstvFo3HGAC2Ef1qZcvBd",
"customer_id": 21569877,
"dropoff_eta": null,
"location_id": 100800,
"spot_user_id": null,
"spot_url_name": "bar-101",
"zone_group_id": 2425,
"zone_group_type": "DINING",
"spot_agent_configs": null,
"zone_agent_configs": null
}
],
"items": [
{
"fee": false,
"tax": 130,
"notes": {},
"comped": false,
"status": "OPEN",
"tab_id": 9200,
"x_name": "BBQ Brisket",
"created": "2025-02-17T13:10:54.209875",
"item_id": 16828,
"options": {},
"order_id": 72970599,
"prepared": null,
"recalled": null,
"subtotal": 1295,
"tax_rate": 0.1,
"discounts": false,
"prep_time": 0,
"x_item_id": null,
"dispatched": null,
"product_id": 17680342,
"router_ids": [2203],
"unit_price": 1295,
"x_metadata": null,
"x_quantity": 1,
"category_id": 55060,
"product_tags": ["60employee"],
"product_type": "DEFAULT",
"product_uuid": "prd_1L4pqUUNkJlDC8cV2d~EtweQ",
"x_product_id": null,
"adjust_reason": null,
"category_name": "Lunch Sandwiches",
"item_subtotal": 1295,
"order_rule_id": null,
"product_delay": null,
"x_item_details": {},
"x_product_name": null,
"product_options": {},
"tax_rate_detail": [
{
"rate": 0.1,
"tax_id": 801,
"weight": 1,
"tax_name": "Tax"
}
],
"true_unit_price": 1295,
"true_x_quantity": 1,
"x_price_level_id": null,
"product_base_price": 1295,
"x_product_base_price": null,
"order_rule_discount_percentage": null
}
],
"adjustments": [],
"payments": [
{
"amount": 1619,
"tip": 130,
"name": "TEST USER",
"autograt": 194,
"created": "2025-02-17T13:10:58.447506-05:00",
"customer_id": 21569877
}
],
"customers": {
"tabOwnerIsPOSUser": false,
"tabOwnerCustomerId": "21569877",
"allCustomersOnTab": [
{
"tab_id": "9200",
"customer_id": "21569877",
"created": "2025-02-17T13:10:51.759Z",
"tab_viewed": "2025-02-17T13:10:51.851Z",
"handle": "+16082139087",
"protocol": "s",
"email": null
}
]
},
"tab_metadata": null
},
"event_type": "ACCRUAL"
}
}
A 200 response would look like the following (note the id which we store in our system for referential integrity and reconciliation):
{
"message": "success",
"id": "hl123hlj12h31"
}
A 4xx response in addition to having the relevant status code should include a message that we log out on our side in order to debug:
{
"message": "Tab missing customer data."
}
REVERSAL:
The REVERSAL type event is an asynchronous event that will be triggered when an applied offer is voided off a tab or a tab with applied offers is fully refunded. At your discretion, this will serve as a way to make these offers available to applied again.
Here's an example request for a REVERSAL type event request:
{
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "example_auth_header_123",
"x-api-key": "null"
},
"json": true,
"body": {
"reversed_offers": ["offer_id_1", "offer_id_2"],
"event_type": "REVERSAL",
"location_id": "1019"
}
}
A 200 response to this will look like this:
{
"reversal_id": 90909
}
A 4xx response to this request should look like this:
{
"message": "Offer reversal failed for this reason."
}