Skip to content

CaptiFi API ​

Pull your guest data straight into your own systems: a booking platform, a CRM, a spreadsheet, or whatever you already use for reporting.

Plan

The API is included on the Growth plan and above. On Essentials the API keys page shows what it does and how to upgrade.

What you can do with it ​

You want toEndpoint
List your venuesGET /venues
Pull guest sign-ups, filtered and paginatedGET /guests
Get visit and opt-in totals for a periodGET /stats
Check which account a key belongs toGET /me
Read your Rewards programme, members, rewards, redemptions and offersGET /rewards/..., see Rewards

The API is read-only. Nothing you do with a key can change your CaptiFi settings, your splash pages, your guest records or your Rewards programme, so it is safe to hand a key to a developer or an agency.

Step 1: create a key ​

Go to API Keys in the sidebar and give your key a name. Name it after the system that will use it, so you can tell your keys apart later.

The API Keys page with the create form, the list of existing keys and the usage example

On Essentials the page explains what the API does and how to get it:

The API Keys page on a plan without API access, showing what it does and an Upgrade to Pro button

Your key appears once, immediately after you create it.

The one-time key reveal, with a copy button and a warning that the key cannot be shown again

Copy it straight away

We store your key encrypted, exactly like a password, so we cannot show it to you again. If you lose it, revoke it and create a new one.

You can hold up to 5 keys at once. Use separate keys for separate systems: if one needs replacing you can revoke it without breaking the others.

Step 2: make a request ​

Send your key as a bearer token in the Authorization header. The base URL is:

https://app.captifi.io/api/v1/customer
bash
curl -H "Authorization: Bearer YOUR_KEY" \
     -H "Accept: application/json" \
     "https://app.captifi.io/api/v1/customer/guests?per_page=50"

Every response is JSON in the same shape: your rows under data, and paging or totals under meta.

json
{
  "data": [
    {
      "id": 84213,
      "venue_id": 17110,
      "name": "Sam Fletcher",
      "first_name": "Sam",
      "last_name": "Fletcher",
      "email": "sam@example.com",
      "phone": "+447700900123",
      "marketing_consent": true,
      "is_return_visit": false,
      "visited_at": "2026-08-18T19:14:00+01:00"
    }
  ],
  "meta": { "current_page": 1, "last_page": 3, "per_page": 50, "total": 132 }
}

Endpoint reference ​

GET /me ​

Confirms which account a key belongs to. Useful as a connection test.

json
{ "data": { "account_id": 41404, "business_name": "Ecton House", "plan": "Pro", "venue_count": 7 } }

GET /venues ​

Your venues, alphabetically.

FieldNotes
idUse this as venue_id on the other endpoints
name, city, countryAs set on My Venues
created_atISO 8601

GET /guests ​

Guest sign-ups, newest first. Only guests who actually arrived are returned, so contacts you imported yourself never appear here as footfall.

ParameterValuesNotes
venue_ida venue idDefaults to every venue on your account
from, toYYYY-MM-DDInclusive
marketing_consent1 or 0Filter to guests you may send marketing to (or may not)
per_page1-200Defaults to 50
page1+Read meta.last_page to know when to stop

marketing_consent says whether you may send the guest marketing now. It is false for a guest who unsubscribed, a guest you suppressed, and a guest whose address bounced or reported your email as spam, even if they ticked the marketing box when they signed in. The filter uses the same answer.

GET /stats ​

Totals for a period, matching the figures on your dashboard.

ParameterValues
venue_ida venue id (defaults to all)
from, toYYYY-MM-DD (defaults to the last 30 days)

visits counts every arrival. unique_guests counts distinct people, matched on email, or on mobile number where the splash page collects a phone instead, so a guest who connected three times in the period counts once. marketing_opt_ins counts arrivals from guests who agreed to marketing and have not since unsubscribed, been suppressed, bounced or complained, on the same rule as marketing_consent.

json
{
  "data": { "from": "2026-07-20", "to": "2026-08-18", "visits": 412, "unique_guests": 388, "marketing_opt_ins": 305 },
  "meta": { "venue_ids": [17110] }
}

Rewards ​

Accounts on Growth and above with Rewards switched on can read their programme with the same keys. The ten endpoints below sit under /rewards, so a members call looks like this:

bash
curl -H "Authorization: Bearer YOUR_KEY" \
     -H "Accept: application/json" \
     "https://app.captifi.io/api/v1/customer/rewards/members?tier=gold&per_page=100"

They are read only, like the rest of the API. Every list comes newest first. The ids are the ones shown on the Rewards pages and carried in the webhook payloads, so a till or CRM can match a redemption to its reference or a member to their member_number.

Who can call them:

  • Any key on the account reads the programme, rewards, redemptions and offers.
  • The three member endpoints (/rewards/members, /rewards/members/{id} and /rewards/members/{id}/ledger) need the person who created the key to hold the View guest data permission, or to be the account owner. A key created by a team member without it gets 403 TEAM_PERMISSION_REQUIRED; granting the permission opens the reads on the next request with the same key. See Team permissions.
  • A key created by a team member who is restricted to some of your venues sees only those venues in meta.venues, and only their redemptions by default.

There are no per-key scopes: a key grants the account's read access in full. That includes each reward's code and display_code, the code a guest shows at the till, on every rewards and redemptions read. Keep a key away from anyone you would not let use the Redeem screen. No token, PIN, key or reward link ever appears in a response, and terminals are not exposed.

An id or venue_id that is not on your account is a 404 with errorREWARD_NOT_FOUND, never a 403, so the API never confirms whether an id exists on another account. A member_id or offer_id filter that is not yours simply returns an empty page.

Paging ​

Every list takes per_page (1 to 200, default 50) and page (1 or more) and returns the same meta as /guests:

json
{ "current_page": 1, "last_page": 4, "per_page": 50, "total": 187 }

A value outside its range is a 422 naming the parameter.

Time windows ​

since, until and updated_since take either a date (YYYY-MM-DD) or an ISO 8601 instant with an offset (2026-09-14T06:30:00-05:00).

  • A bare date is a day in UK time (Europe/London), whatever time zone the venue is in. since=2026-09-14 starts at midnight in London.
  • A bare date on until means the end of that day, so since=2026-09-01&until=2026-09-01 is that whole day.
  • An instant with an offset is converted exactly: until=2026-09-14T06:30:00-05:00 and until=2026-09-14T11:30:00Z mean the same moment.
  • since later than until is a 422 on since.

GET /rewards/programme ​

The programme as set up under Rewards → Setup: earn rates, bonuses, tiers, expiry and caps. data is null until you have created a programme. meta.venues lists the venues on the programme that the key may see.

FieldNotes
modepoints or stamps
default_currency, point_valuesISO currency codes in upper case. point_values maps a currency to what a point costs you in it, as entered under Points, value and limits
points_per_visit, points_per_currency_unit, signup_bonus_points, birthday_bonus_pointsThe earn rates and bonuses
points_expire_after_monthsnull when left blank, which means 12 months
signup_offer_id, birthday_offer_idThe offers given on joining and on birthdays, or null
tiersEach has key, name, min_points_12m, multiplier and colour
membership_optin_modecheckbox (the Join box on sign-in) or auto (connecting joins the guest)
self_redeem_cap_minor, max_spend_per_earn_minorThe Use now cap and the max sale per earn, in minor units (pence or cents)
daily_earn_cap_pointsOr null for no cap
service_hoursopen and close as HH:MM, or null
meta.venues[].currency, earn_multiplier, is_active, mirror_to_leatAs set on the venue row
meta.venues[].timezone, timezone_is_defaultThe venue's time zone. timezone_is_default is true while the venue is still on the default (UTC), which the Setup page warns about
json
{
  "data": {
    "id": 12,
    "name": "Harbour Rewards",
    "mode": "points",
    "default_currency": "GBP",
    "point_values": { "GBP": 1 },
    "points_per_visit": 10,
    "points_per_currency_unit": 1,
    "signup_bonus_points": 25,
    "birthday_bonus_points": 50,
    "points_expire_after_months": 12,
    "signup_offer_id": 42,
    "birthday_offer_id": null,
    "tiers": [
      { "key": "regular", "name": "Regular", "min_points_12m": 0, "multiplier": 1, "colour": null },
      { "key": "gold", "name": "Gold", "min_points_12m": 500, "multiplier": 1.5, "colour": "#d4a017" }
    ],
    "reward_label": "reward",
    "welcome_text": "Show this page at the bar to claim your reward",
    "terms_url": "https://example.com/rewards-terms",
    "brand": {
      "programme_name": "Harbour Rewards",
      "background_colour": "#0b3d91",
      "foreground_colour": "#ffffff",
      "logo_path": null,
      "strip_path": null
    },
    "membership_optin_mode": "checkbox",
    "deliver_by_sms": false,
    "self_redeem_cap_minor": 500,
    "max_spend_per_earn_minor": 50000,
    "daily_earn_cap_points": 200,
    "service_hours": { "open": "11:00", "close": "23:00" },
    "is_active": true,
    "activated_at": "2026-06-02T10:15:00+01:00",
    "created_at": "2026-06-01T09:00:00+01:00",
    "updated_at": "2026-09-10T16:42:11+01:00"
  },
  "meta": {
    "venues": [
      {
        "site_id": 17110,
        "site_name": "The Harbour Inn",
        "currency": "GBP",
        "earn_multiplier": 1,
        "is_active": true,
        "mirror_to_leat": false,
        "timezone": "Europe/London",
        "timezone_is_default": false
      },
      {
        "site_id": 17138,
        "site_name": "The Loop",
        "currency": "USD",
        "earn_multiplier": 1.5,
        "is_active": true,
        "mirror_to_leat": false,
        "timezone": "America/Chicago",
        "timezone_is_default": false
      }
    ]
  }
}

GET /rewards/members ​

Members of the programme, newest first. Without a status filter every member is returned, including those who have left.

ParameterValuesNotes
emailan email addressExact match, case-insensitive
phonea phone numberExact match. A number with its country code, +447700900123, matches as it is. A national number such as 07700 900123 or (212) 555-1234 is read against the country of each venue on your programme and matches any reading, so one call covers a London venue and a Chicago one
tiera tier keyAs in the programme's tiers
statusactive, left, anonymised
updated_sincedate or instantMembers changed on or after it, see Time windows
per_page, pageSee Paging
FieldNotes
member_numberThe ten-character number on the member's card and in the webhooks
email, phone_e164, first_name, last_name, date_of_birthThe member's details. date_of_birth is YYYY-MM-DD
points_balance, lifetime_points, points_12mThe live balance, the points earned in all time, and the points earned in the last 12 months that decide the tier
tier_key, tier_reached_atnull for an untiered member
rewards_availableHow many rewards the member can use right now: issued, not yet expired and with a use left. Counts rewards, not uses
first_site_id, last_visit_site_idThe venue they joined at and the venue of their last visit
joined_viasplash, import, staff or api
membership_consent_atWhen they agreed to join
left_at, anonymised_atSet when the member left the programme, or was erased under GDPR

Contact details are masked for an account in limited access, and for members who joined at a venue the key holder has limited access to: email, phone_e164 and first_name are partly replaced with asterisks, and last_name and date_of_birth are null. Points, tier and visit fields are unaffected. This is the same rule /guests applies to a guest row.

json
{
  "data": [
    {
      "id": 917,
      "member_number": "M7K3Q9P2XA",
      "email": "sam@example.com",
      "phone_e164": "+447700900123",
      "first_name": "Sam",
      "last_name": "Fletcher",
      "date_of_birth": "1990-05-04",
      "points_balance": 543,
      "lifetime_points": 1210,
      "points_12m": 860,
      "tier_key": "gold",
      "tier_reached_at": "2026-07-14T20:02:11+01:00",
      "rewards_available": 2,
      "visits_count": 38,
      "last_visit_at": "2026-09-19T19:41:00+01:00",
      "last_visit_site_id": 17110,
      "first_site_id": 17110,
      "joined_via": "splash",
      "membership_consent_at": "2026-03-02T18:20:45+00:00",
      "left_at": null,
      "anonymised_at": null,
      "created_at": "2026-03-02T18:20:45+00:00"
    }
  ],
  "meta": { "current_page": 1, "last_page": 19, "per_page": 50, "total": 912 }
}

GET /rewards/members/{id} ​

One member, the same fields under data, with the same masking. There is no meta.

GET /rewards/members/{id}/ledger ​

The member's points ledger, newest first, with their live balances in meta beside the paging fields.

ParameterValuesNotes
reasonearn_visit, earn_spend, earn_signup, earn_birthday, earn_bonus, earn_manual, redeem, expire, adjust, reverse
since, untildate or instantOn the entry time, see Time windows
per_page, pageSee Paging

The reasons match the ledger on the member's page: Visit, Spend, Sign-up bonus, Birthday bonus, Bonus, Awarded by staff, Redeemed, Expired, Adjusted and Reversed. delta is the points moved, negative when points left the balance; balance_after is the balance once the line applied; spend_minor and currency are filled in when the earn came from an order or a till sale; reference ties the line to what caused it, such as spend:toast:70211 or redeem:3311.

json
{
  "data": [
    {
      "id": 88041,
      "member_id": 917,
      "site_id": 17110,
      "delta": 35,
      "balance_after": 543,
      "reason": "earn_spend",
      "reference": "spend:toast:70211",
      "spend_minor": 2380,
      "currency": "GBP",
      "note": null,
      "created_by_user_id": null,
      "terminal_id": null,
      "created_at": "2026-09-19T20:14:09+01:00"
    },
    {
      "id": 87990,
      "member_id": 917,
      "site_id": 17110,
      "delta": 10,
      "balance_after": 508,
      "reason": "earn_visit",
      "reference": "visit:17110:2026-09-19",
      "spend_minor": null,
      "currency": null,
      "note": null,
      "created_by_user_id": null,
      "terminal_id": null,
      "created_at": "2026-09-19T19:41:00+01:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 3,
    "per_page": 50,
    "total": 121,
    "points_balance": 543,
    "lifetime_points": 1210,
    "points_12m": 860,
    "tier_key": "gold",
    "tier_reached_at": "2026-07-14T20:02:11+01:00"
  }
}

GET /rewards/rewards ​

Issued rewards across the account, newest first.

ParameterValuesNotes
statusissued, redeemed, expired, voidedMatches the stored status, see the note below
offer_idan offer id
member_ida member id
venue_ida venue idThe venue the reward was issued at. A reward issued programme-wide has no venue and is left out when you filter by one
since, untildate or instantOn issued_at, see Time windows
per_page, pageSee Paging

In the response, an issued reward whose expires_at has passed reads expired. The status filter matches the stored status, so such a reward is returned by status=issued and reads expired in the row.

FieldNotes
code, display_codeThe eight-character code and its display form with a hyphen, 7F3K-9QPX. Visible to every key on the account
kind, percent, amount_minor, currency, free_item_label, min_spend_minor, terms, site_idsA snapshot of the offer when the reward was issued. kind is percent, fixed, free_item or custom; site_ids is null for a reward valid at every programme venue
uses_remaining1 while unused, 0 once redeemed
issued_viaconnect, automation, campaign, sms, manual, bulk, points, api or import
points_spentThe points a member paid to claim it from the catalogue, otherwise null
is_locked, locked_untilSet for 60 minutes after five wrong PIN entries against the reward
redeemed_at, redemption_id, redeemed_at_site, redeemed_via, reference, redeemed_by_initialsFilled in once the reward is redeemed, null before
delivered_email_at, delivered_sms_at, viewed_at, view_countWhen the reward was sent; viewed_at is the first open of the reward page and view_count counts every open
json
{
  "data": [
    {
      "id": 5123,
      "offer_id": 42,
      "offer_name": "Welcome drink",
      "offer_headline": "A free house drink on us",
      "member_id": 917,
      "site_id": 17110,
      "site_name": "The Harbour Inn",
      "code": "7F3K9QPX",
      "display_code": "7F3K-9QPX",
      "kind": "free_item",
      "percent": null,
      "amount_minor": null,
      "currency": "GBP",
      "free_item_label": "A house drink",
      "min_spend_minor": null,
      "terms": "One per person. Not valid with other offers.",
      "site_ids": null,
      "status": "redeemed",
      "uses_remaining": 0,
      "issued_at": "2026-09-18T12:05:40+01:00",
      "expires_at": "2026-09-25T23:59:59+01:00",
      "issued_via": "connect",
      "points_spent": null,
      "is_locked": false,
      "locked_until": null,
      "redeemed_at": "2026-09-19T20:14:08+01:00",
      "redemption_id": 3311,
      "redeemed_at_site": "The Harbour Inn",
      "redeemed_via": "staff_scan",
      "reference": "RDM-7F3K9Q",
      "redeemed_by_initials": "RB",
      "delivered_email_at": "2026-09-18T12:05:42+01:00",
      "delivered_sms_at": null,
      "viewed_at": "2026-09-19T20:13:50+01:00",
      "view_count": 2
    }
  ],
  "meta": { "current_page": 1, "last_page": 12, "per_page": 50, "total": 587 }
}

GET /rewards/rewards/{id} ​

One reward, the same fields under data.

GET /rewards/redemptions ​

Every redemption at the venues the key may see, newest first. There is no default window: page through the history as you would for guests, or pass since.

ParameterValuesNotes
venue_ida venue id
since, untildate or instantOn the redemption time, see Time windows
statuscompleted, voided
channelstaff_scan, staff_code, terminal_pin, pos_api, self_serve, manager_manual
per_page, pageSee Paging
FieldNotes
referenceRDM- and six characters, the reference on the guest's screen and in your log
reward_codeThe redeemed reward's display code
channelHow it was redeemed
redeemed_by_user_id, redeemed_by_name, staff_initials, terminal_id, terminal_nameWho honoured it: the staff login, the initials they typed, or the terminal
face_value_minor, basket_total_minor, discount_applied_minor, currencyMoney in minor units in the venue's currency
points_spent, points_awardedPoints the member spent on the reward, and points the sale awarded
reasonThe reason or note staff recorded at redemption, or null
status, voided_at, voided_by_user_id, void_reason, restored_at, restored_by_user_idvoided once a manager voids it; the restored fields are set if it was later restored
json
{
  "data": [
    {
      "id": 3311,
      "reference": "RDM-7F3K9Q",
      "offer_id": 42,
      "offer_name": "Welcome drink",
      "member_reward_id": 5123,
      "reward_code": "7F3K-9QPX",
      "member_id": 917,
      "site_id": 17110,
      "site_name": "The Harbour Inn",
      "channel": "staff_scan",
      "redeemed_by_user_id": 41404,
      "redeemed_by_name": "Rosa Bright",
      "terminal_id": null,
      "terminal_name": null,
      "staff_initials": "RB",
      "face_value_minor": 450,
      "currency": "GBP",
      "basket_total_minor": 2380,
      "discount_applied_minor": 450,
      "points_spent": 0,
      "points_awarded": 35,
      "pos_reference": null,
      "reason": null,
      "status": "completed",
      "voided_at": null,
      "voided_by_user_id": null,
      "void_reason": null,
      "restored_at": null,
      "restored_by_user_id": null,
      "created_at": "2026-09-19T20:14:08+01:00"
    }
  ],
  "meta": { "current_page": 1, "last_page": 7, "per_page": 50, "total": 331 }
}

GET /rewards/redemptions/{id} ​

One redemption, the same fields under data.

GET /rewards/offers ​

Your offers, newest first. Archived offers are left out unless you ask for them with status=archived.

ParameterValuesNotes
statusdraft, active, paused, archived
per_page, pageSee Paging
FieldNotes
kind, percent, amount_minor, currency, free_item_label, min_spend_minorWhat the offer gives. kind is percent, fixed, free_item or custom
estimated_value_minorWhat it costs you per reward, in minor units
public_codeThe shared discount code, such as SUMMER20, or null
points_cost, show_in_catalogueThe catalogue price and whether members can claim it with points
site_idsThe venues it is valid at, or null for every programme venue
issue_on_connect, connect_frequency, connect_every_daysIssue on WiFi sign-in: once, per_visit or every_n_days
valid_days_after_issue, starts_at, ends_atExpiry after issue and the offer window
max_issues, issued_count, max_redemptions_total, redemptions_count, claims_count, max_per_member, cooldown_daysLimits and running totals
self_redeem, channelsWhether guests may tap Use now, and the ways staff may honour it
json
{
  "data": [
    {
      "id": 42,
      "programme_id": 12,
      "name": "Welcome drink",
      "headline": "A free house drink on us",
      "description": "Claim a house drink on your first visit.",
      "terms": "One per person. Not valid with other offers.",
      "kind": "free_item",
      "percent": null,
      "amount_minor": null,
      "currency": "GBP",
      "estimated_value_minor": 450,
      "free_item_label": "A house drink",
      "min_spend_minor": null,
      "public_code": null,
      "points_cost": null,
      "show_in_catalogue": false,
      "site_ids": null,
      "issue_on_connect": true,
      "connect_frequency": "once",
      "connect_every_days": null,
      "valid_days_after_issue": 7,
      "starts_at": null,
      "ends_at": null,
      "max_issues": null,
      "issued_count": 587,
      "max_redemptions_total": null,
      "redemptions_count": 331,
      "claims_count": 0,
      "max_per_member": 1,
      "cooldown_days": null,
      "self_redeem": false,
      "channels": ["staff_scan", "staff_code", "terminal_pin", "pos_api"],
      "promotion_id": null,
      "sort_order": 0,
      "status": "active",
      "created_at": "2026-06-01T09:30:00+01:00",
      "updated_at": "2026-09-01T08:12:40+01:00"
    }
  ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 50, "total": 4 }
}

GET /rewards/offers/{id} ​

One offer, plus a stats block the list does not carry. An archived offer still answers here.

json
"stats": { "issued": 587, "outstanding": 118, "redeemed": 331, "voided": 4, "expired": 134, "claimed": 0 }

outstanding counts issued rewards that have not yet expired, redeemed and voided count redemptions, expired counts rewards that ran out, and claimed counts rewards members bought with points.

Rate limits ​

120 requests per minute, per key. Every response also carries rate limit headers, but they report the wider per-address ceiling described below, not the per-key allowance:

HeaderMeaning
X-RateLimit-LimitThe per-address ceiling, 600 for requests that carry a key
X-RateLimit-RemainingCalls left under that ceiling in the current minute, across every key calling from your address
Retry-AfterOn a 429, the seconds to wait before retrying

The per-key allowance of 120 is enforced but not shown in a header, so a client pacing itself from X-RateLimit-Remaining alone can still meet a per-key 429. Keep a single key under 120 calls a minute by design, and treat Retry-After as the signal when you go over.

If you go over, the API answers 429. Wait for the number of seconds in Retry-After before retrying, rather than retrying straight away: the window is a rolling minute, so a 429 can mean waiting up to 60 seconds.

The wider ceiling of 600 requests per minute per IP address is shared by every key calling from that address. It sits well above the per-key limit so that running several integrations from one office or server does not reach it, and you will only see it if something is looping. Calls that arrive with no Authorization header at all are limited far more tightly, so a misconfigured client that omits the header will start seeing 429 instead of 401 quickly: if that happens, fix the header rather than retrying.

The quota is counted per key, not per IP address, so one system going over never uses up another system's allowance. Pulling a large history is best done with per_page=200 and paging through, rather than many small requests.

Revoking a key ​

Click Revoke next to any key. It stops working immediately, and anything using it will start getting 401. This cannot be undone, so if you are replacing a key in a live system, create the new one first.

Common responses ​

StatusMeaningWhat to do
200Success-
401The key is missing, wrong or revokedCheck the Authorization header
403Your plan does not include the API, or the credential is not an API keyUpgrade to Growth, or create a proper API key
403REWARDS_DISABLED: Rewards is switched off for your accountThe account owner switches it on under Integrations, Dashboard features
403TEAM_PERMISSION_REQUIRED: the key was created by a team member whose role lacks the permission the endpoint needs; meta.permission names itGrant the permission on the Team page, or use a key the account owner created
404The venue_id is not on your accountCheck GET /venues
404REWARD_NOT_FOUND: a Rewards id, or the venue_id on a Rewards endpoint, is not on your accountCheck the id against the matching list endpoint
422A parameter is invalidThe response names the parameter
429Rate limitedWait, then retry

Every response is JSON, including errors, whether or not your client sends the Accept: application/json header.

An error body always carries a message you can show to a person. A 401 and a 403 additionally carry a short error code you can branch on, which is the pair worth handling in code because they mean "fix your credential":

json
{
  "message": "Unauthenticated.",
  "error": "unauthenticated"
}

The codes are unauthenticated (401), and plan_upgrade_required or customer_api_key_required (403). A plan_upgrade_required answer also carries meta.feature (customer_api, or rewards when the plan lacks Rewards) and meta.required_plan. On the Rewards endpoints a 403 can also be REWARDS_DISABLED or TEAM_PERMISSION_REQUIRED, and a 404 is REWARD_NOT_FOUND; those three are spelt in upper case, the others in lower case. A request to a path this API does not expose returns endpoint_not_found.

json
{
  "message": "Rewards is switched off for this account. Enable it from the Integrations page under Dashboard features.",
  "error": "REWARDS_DISABLED"
}

A 422 carries an errors object keyed by parameter name instead, in Laravel's standard validation shape. A 404 for an unknown venue_id on /guests or /stats, and a 429, carry message only, so branch on the status code for those.

Security ​

  • Treat a key like a password: never put it in front-end code, a public repo, or a shared document.
  • Keys are scoped to your account. A key can only ever read your own venues, your own guests and your own Rewards programme.
  • A key reads reward codes. Anyone holding one can list every issued reward with the code a guest would show at the till, so keep keys away from anyone you would not let use the Redeem screen.
  • A key cannot be used to sign in to the dashboard, and it cannot change anything.
  • If a key may have leaked, revoke it. That is instant and cannot be undone.

Your responsibilities under GDPR ​

Guest and Rewards member records include personal data. Once you pull them into your own system you are the controller of that copy: your own retention rules, security and deletion requests apply to it. Deleting or anonymising a guest in CaptiFi does not delete the copy you exported. See GDPR & Data Protection.

See also ​

CaptiFi — Guest WiFi Marketing Platform