Skip to content
Type to search…

GraphQL API introduction

Learn how to query the GoTab GraphQL API — from your first request to efficient filtering with fiscalDay.

The GoTab GraphQL API lives at https://gotab.io/api/graph and uses the same Bearer token as the REST API. This guide walks through making your first query, reading catalog and tab data, and filtering efficiently — including the indexing patterns that keep queries fast.

For a comparison of when to use GraphQL versus REST, see the overview page.


If you haven’t used GraphQL before: it’s a query language where you describe exactly the data you want, and the server returns precisely that — no more, no less. Instead of hitting multiple REST endpoints and stitching results together, you write one query that fetches nested data in a single request.

Every GoTab GraphQL request is a POST to the same URL. You send a query string in the request body, and the server returns a JSON object with your results under a data key.

GraphQL is best for reading — catalog sync, reporting, order history, and anything that benefits from fetching related data together. REST is still the right tool for actions like creating tabs.


Terminal window
curl -X POST https://gotab.io/api/graph \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"query": "{ locationsList { locationUuid name } }"
}'

Response:

{
"data": {
"locationsList": [
{ "locationUuid": "loc_abc123", "name": "Main Street" },
{ "locationUuid": "loc_def456", "name": "Downtown" }
]
}
}

All results are under data. If something goes wrong, an errors array appears alongside data — your data fields will be null for anything that failed.


The interactive explorer at docs.gotab.io/api-reference/graphql lets you browse the full schema, autocomplete field names, and run queries live against your sandbox.

To authenticate:

  1. Open the Headers panel in the explorer.
  2. Add: Authorization: Bearer YOUR_TOKEN
  3. Run any query — results appear in the right panel.

Use your sandbox credentials here so you’re not hitting production while exploring.


Two query styles: List queries vs single-record lookups

Section titled “Two query styles: List queries vs single-record lookups”

You’ll notice the schema has two forms for most resources:

  • tabsList(...) — returns an array, supports filtering and pagination
  • tab(tabId: BigInt!) or tabByTabUuid(tabUuid: String!) — returns a single record by ID

Use list queries when pulling sets of data for reporting or sync. Use single-record lookups when you already have an ID and just need the details for that one thing.

# Single record lookup
{
tabByTabUuid(tabUuid: "nIJHITr9GU1U9zNCakTdk9iA") {
tabUuid
status
total
created
}
}

Menus, categories, and products are each queryable as flat top-level lists. Filter them by locationId (the numeric ID, not the UUID) using the condition argument:

{
menusList(condition: { locationId: 12345 }) {
menuId
name
startTime
endTime
}
}

To get categories for a specific menu:

{
categoriesList(condition: { locationId: 12345 }) {
categoryId
label
xCategoryId
}
}

To get products:

{
productsList(condition: { locationId: 12345 }) {
productId
name
price
available
description
}
}

tabsList returns tabs for a location. Filter by locationId using condition:

{
tabsList(condition: { locationId: 12345 }) {
tabUuid
status
total
created
fiscalDay
ordersList {
orderId
orderUuid
status
total
itemsList {
name
quantity
unitPrice
subtotal
}
}
}
}

Filtering efficiently — use fiscalDay, not timestamp ranges

Section titled “Filtering efficiently — use fiscalDay, not timestamp ranges”

GoTab organizes transactional data around fiscal days — date strings in YYYY-MM-DD format that represent a location’s business day (which may not align with midnight UTC). The fiscalDay field is indexed; arbitrary timestamp ranges are not.

The pattern that works:

{
tabsList(
condition: { locationId: 12345 }
filter: { fiscalDay: { equalTo: "2024-01-15" } }
) {
tabUuid
status
total
fiscalDay
}
}

For a date range, use greaterThanOrEqualTo and lessThanOrEqualTo together:

{
tabsList(
condition: { locationId: 12345 }
filter: {
fiscalDay: {
greaterThanOrEqualTo: "2024-01-01"
lessThanOrEqualTo: "2024-01-31"
}
}
) {
tabUuid
status
total
fiscalDay
created
}
}

The same applies to ordersList and ledger queries — filter on fiscalDay first, then narrow further if needed.

A fiscal day is the date GoTab assigns to a business transaction, based on when the location’s business day started — not UTC midnight. A tab opened at 11:30 PM and closed at 1:00 AM the next calendar day will both belong to the same fiscal day. If you’re building a daily report, always query by fiscalDay rather than trying to calculate the right UTC window.

You can look up which fiscal day a specific timestamp belongs to using:

{
goFiscalDay(
_locationId: 12345
_utcTimestamp: "2024-01-15T02:30:00Z"
)
}

This returns the fiscalDay date string for that moment at that location.


Ledger entries for reporting and reconciliation

Section titled “Ledger entries for reporting and reconciliation”

For payment-level reporting (tip amounts, payment methods, refunds, order source), use ledgerEntriesList or realTimeLedgerEntriesList rather than querying tabs directly.

  • ledgerEntriesList — settled data, best for end-of-day reports and accounting exports
  • realTimeLedgerEntriesList — reflects live state including open tabs, useful for dashboards

Both support fiscalDay filtering:

{
ledgerEntriesList(
condition: { locationId: 12345 }
filter: { fiscalDay: { equalTo: "2024-01-15" } }
) {
tabUuid
total
tax
tip
fiscalDay
created
}
}

Identifying order source (server vs customer-placed)

Section titled “Identifying order source (server vs customer-placed)”

The pointOfInteraction field on the Payment type indicates whether an order was placed at a server terminal (POS) or by a customer via QR:

{
paymentsList(
condition: { locationId: 12345 }
filter: { fiscalDay: { equalTo: "2024-01-15" } }
) {
paymentId
amount
tip
pointOfInteraction
created
}
}
  • "SERVER" — placed through the POS by a staff member
  • "CONSUMER" — placed by a guest via QR or web

For large result sets, use first to limit the page size and offset for simple offset pagination:

{
tabsList(
condition: { locationId: 12345 }
filter: { fiscalDay: { equalTo: "2024-01-15" } }
first: 50
offset: 0
) {
tabUuid
status
total
}
}

Increment offset by first on each subsequent request to page through results. For cursor-based pagination (more efficient for large datasets), see Pagination.


The field for GoTab staff members is employeesList, not usersList:

{
employeesList(condition: { locationId: 12345 }) {
employeeUuid: userRoleUuid
name
roleName
}
}

The condition vs filter argument — what’s the difference?

Section titled “The condition vs filter argument — what’s the difference?”

Both arguments narrow results, but they work differently:

  • condition matches rows where a field equals an exact value. It maps directly to indexed columns and is always fast. Use it to scope to a location: condition: { locationId: 12345 }.

  • filter supports range operators (greaterThanOrEqualTo, lessThanOrEqualTo, equalTo, in, etc.) and can combine multiple conditions. Use it for date ranges and non-equality checks.

The most efficient queries combine both — condition to hit the index, filter to narrow within it:

tabsList(
condition: { locationId: 12345 } # index hit
filter: { fiscalDay: { equalTo: "2024-01-15" } } # narrow within
)

Avoid using filter alone for location scoping — always put locationId in condition.


CodeCauseAction
401Token expiredRefresh with your refresh_token and retry
403Token valid but no access to that locationCheck allowed_location_ids on the credential, or re-run the OAuth authorization flow for that location
Timeout / empty resultQuery missing fiscalDay filter on a large tableAdd filter: { fiscalDay: { equalTo: "..." } } to scope the query
Schema error on field nameField doesn’t exist (e.g. usersList)Check the GraphQL Explorer for the correct field name

For general error handling patterns, see Error Handling.