Skip to content

List members of one membership provider

GET
/v1/members
curl --request GET \
--url 'https://api.scanfood.co/ext/v1/members?providerId=eExLLv1C1CKb6bQ9GH2S&limit=100' \
--header 'Authorization: Bearer <token>'

Returns the loyalty members of a single membership provider, oldest sign-up first. Take providerId from /v1/shops; several branches of a franchise can share one provider, so a member belongs to the provider, not to a branch.

Paging is the only way to read the whole base. nextCursor is an opaque member token — pass it straight back as cursor. Keep going until nextCursor is null.

Incremental sync is not available yet. Sending updatedSince returns 400 not_supported. Every writer stamps a change timestamp today, but roughly three quarters of the existing member records predate that stamp, so an incremental read would silently leave them out. Until those records are backfilled, run a full sync (page through with cursor) on whatever schedule you need.

providerId
required
string

Membership provider to read. Must be the providerId of a shop returned by /v1/shops.

updatedSince
string format: date-time

Not available yet — sending this returns 400 not_supported. See the endpoint description.

limit
integer
default: 100

Rows per page. Out-of-range values are never rejected, they are adjusted silently: any fraction is truncated first, then anything above 200 is reduced to 200 and anything that is left below 1 (0, negative, a fraction under 1, text, missing) falls back to 100.

cursor
string

The nextCursor from the previous page (an opaque member token, e.g. m1_Zm9vYmFyYmF6).

OK

Media typeapplication/json
object
ok
required
boolean
data
required
Array<object>

One loyalty member. Personal profile fields are deliberately left out: no linked social account identifier, no birth date, no sex, no photo, no internal staff notes, and no per-bill history. Phone and email are reported only as the booleans hasTel/hasEmail, unless your key is explicitly allowed to read them (see tel/email below).

object
memberId
required

Opaque, stable member token (prefix m1_). The same value appears on bills, so you can join the two. It is not the customer’s real identifier and cannot be reversed.

string | null
rank
required

Membership rank code recorded on the member, e.g. BRONZE. null when none is recorded.

string | null
rankLevel
required

Tier index (a whole number, 0-based) within the provider’s tier list. Meaningful only when the provider runs automatic tier upgrades. null when the member has no valid tier index recorded. Use rankName for display.

number | null
rankName
required

Display name of the tier the member is on right now. null if the provider has no tiers configured.

string | null
points
required

Points available to spend, after expiry has been applied. This is not the sum of everything ever earned.

number
credit
required

Store credit available to spend, after expiry has been applied. Always 0 when creditEnabled is false.

number
creditEnabled
required

Whether this provider runs the store-credit programme at all.

boolean
coupons
required

Coupons the member holds, including ones already used.

Array<object>

One coupon the member is holding or has already used.

object
code
required

The short code the member shows at the counter. Unique per issued coupon.

string | null
status
required

available = still usable, used = already redeemed.

string | null
templateId
required

The coupon definition this was issued from. Several coupons share one template.

string | null
name
required

Coupon name as the member sees it.

string | null
expireAt
required

Expiry, ISO-8601 with a +07:00 (Thailand) offset — membership bases do not carry a timezone of their own, so member times are always Thailand time. null means no expiry was set.

string | null
labels
required

Free-text tags the shop put on this member.

Array<string>
persona
required

Free-text persona tags the shop put on this member.

Array<string>
createdDate
required

When the member signed up, ISO-8601 with a +07:00 (Thailand) offset — membership bases do not carry a timezone of their own, so member times are always Thailand time.

string | null
visitCount
required

How many recorded visits the member has. The visits themselves are not returned.

integer
lastVisit
required

Date of the most recent recorded visit (YYYY-MM-DD). null if there are none.

string | null
hasTel
required

Whether a phone number is on file. The number itself is not returned unless your key is allowed to read it.

boolean
hasEmail
required

Whether an email address is on file.

boolean
hasLine
required

Whether the member is linked to a LINE account.

boolean
tel

Phone number. Present only if your key is explicitly allowed to read personal contact details; ask ScanFood to enable it.

string | null
email

Email address. Present only if your key is explicitly allowed to read personal contact details.

string | null
nextCursor

Pass back as cursor to fetch the next page. null means no more rows.

string | null
serverTime
required

Server clock when the response was produced (UTC, ISO-8601).

string format: date-time
Example
{
"ok": true,
"serverTime": "2026-09-09T13:45:12.310Z"
}
X-RateLimit-Limit
integer

Daily quota for this key — requests allowed per Thailand-time calendar day. This is not the per-minute burst limit; hitting that limit returns rate_limited with a Retry-After header, and is not reflected in these two headers. Not sent on every response. The pair is added by the daily meter, which runs after the key check and after the per-minute brake, so 401, 503, and 429 rate_limited carry neither header. Everything that reaches the meter — including 429 quota_exceeded and 4xx/5xx raised by the handler itself — carries both.

X-RateLimit-Remaining
integer

Requests left in the current Thailand-time day for this key, after this request. Sent under the same conditions as X-RateLimit-Limit.

Malformed request. error is one of: bad_query (missing/invalid parameters, both or neither selection mode, range over 31 days, a lineUserId that is not shaped like a LINE user id), bad_cursor (the cursor is not valid, or points at a row that no longer exists), not_supported (a parameter that exists in this document is not available yet — today only updatedSince on /v1/members; the message says what to do instead).

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "bad_query",
"message": "choose exactly one mode: from+to (calendar days) or updatedSince (ISO-8601)"
}

Missing or unusable key. error is invalid_key (absent, malformed, or wrong secret — the API does not reveal whether the key id exists) or key_revoked (the key was revoked).

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "invalid_key",
"message": "Invalid API key"
}

The requested scope is not yours. error is one of: shop_not_in_scope (the requested shopId is outside this key’s scope), provider_not_in_scope (the requested providerId is outside this key’s scope), lookup_not_enabled (this key is not allowed to look members up by LINE user id; that permission is granted per key and is off by default).

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "shop_not_in_scope",
"message": "this API key cannot access that shopId"
}

rate_limited — too many requests per minute for this key. The body carries retryAfterSec, limit (the per-minute ceiling that was hit) and windowSec (the window it was measured over), and the response also has a Retry-After header. X-RateLimit-* are not sent on this one, because the per-minute brake sits in front of the daily meter. quota_exceeded — the daily quota for this key is used up; it resets at 00:00 Thailand time (ICT). This one does carry X-RateLimit-*.

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "quota_exceeded",
"message": "daily quota exceeded (10000 requests/day, Thailand time). Resets at 00:00 ICT."
}
Retry-After
integer

Seconds to wait before trying again. Sent on 429 rate_limited (the per-minute brake). Not sent on 429 quota_exceeded, where the wait is until the next Thailand-time day.

internal — temporary failure on our side. Retry with backoff.

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "internal",
"message": "temporary failure, please retry"
}

auth_unavailable — the store we check keys against could not be read, so the request was refused instead of being let through. Your key is fine; this is our side. Retry with backoff. X-RateLimit-* are not sent on this response.

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "auth_unavailable",
"message": "Authentication service temporarily unavailable, retry later"
}