EYalla/Developers Contact us

Access Control API (v1)

The Access Control API connects door hardware to a studio that runs on EYalla. A card reader, turnstile controller or face terminal asks whether a member may come in, and EYalla answers granted or denied. Between scans, your system can read the studio's member list, for example to enrol faces or cards on a terminal.

It is available to studios that have the Access Control add-on switched on.

This is not the Merchant API. Requests go to the studio's own web address, not to api.eyalla.net, and they authenticate with an X-API-Key and X-API-Secret pair, not a Bearer token. Keys from one API never work on the other.


What the studio sends you

The studio creates your credentials in its EYalla admin under Bookings → Access API keys. There it can issue a key, change its secret, revoke it or delete it, and every scan you send appears in its access logs. Ask for one key per integration, so the studio can switch one vendor off without affecting another.

Item What it is
Base address https://<studio address>/api/v1/external. Every path on this page is added to it.
API key ak_ followed by 32 letters and digits. It names your integration.
API secret 48 letters and digits. The studio sees it only once, when the key is made or the secret is changed, and should send it to you separately from the key.
Permissions Which calls the key may make (see Permissions).
Hourly limit Requests the key may make per hour. The default is 1,000.

How it works

  1. Scan. The member shows their wallet card, taps an access card, or is recognised by a face terminal.
  2. Ask. Your device sends the code it read to POST /access/verify.
  3. Decide. EYalla checks the account and the membership's dates and pauses, then records the scan in the studio's access log.
  4. Act. Open the door only when status is granted. Keep it shut for anything else.
Call Permission Purpose
POST /access/verify access.verify Decide one entry, or record one exit.
GET /members members.read Read the studio's members, one page at a time.
GET /members/{uuid} members.read Read one member.

Authentication

Send every request over HTTPS with both credential headers. The key says who you are; the secret proves it. Send and accept JSON.

X-API-Key: ak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-API-Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json
Content-Type: application/json
  • Keep the secret on a server or controller where it can be changed. Do not build it into device firmware that cannot be updated.
  • If the studio changes the secret, the old one stops working at once. Ask for the new one.
  • A key the studio has revoked, or one past its expiry date, is refused with 403 key_inactive.
  • The secret is checked before anything else about the key is revealed. A wrong secret and an unknown key give the same answer.

Permissions

Permission Allows
access.verify Checking members at the door, and recording exits.
members.read Reading the member list and single members.
* Every permission, including any added to this API later.

A key that lacks the permission for a call is refused with 403 missing_scope, and the message names the permission it needs. A door reader that only checks entries needs only access.verify.


Checking a member at the door

POST /access/verify

Send one request for each scan. Once a request has passed authentication, the studio's add-on is on and the body is valid, the answer is always HTTP 200, and the decision is in the body.

Request

Field Required Values Notes
credential_type Yes wallet, card or qr What credential_id is (see below).
credential_id Yes Text, up to 255 characters The code exactly as your device read it.
action No entry (default) or exit See Entry and exit.
device_id No Text, up to 100 characters Your name for the reader, e.g. front-door-01. It is kept with the scan in the studio's log. It may instead be sent as the X-Device-ID header.
{
  "credential_type": "wallet",
  "credential_id": "Hq3Tz8kLmP0aWc5RuY2vNd7XbE4sJf1GtK9oQiA6",
  "action": "entry",
  "device_id": "front-door-01"
}

What each credential is

Type credential_id is Where it comes from
wallet The code in the barcode of the member's membership card: 40 letters and digits. Send it exactly as your scanner read it. The card in Apple Wallet and the studio's web card show it as a QR code. Google Wallet shows it as a Code 128 barcode. Your scanner must read both. When the studio revokes or reissues a card, the card gets a new code and the old code is refused. Loyalty cards never open the door.
card The access-card number paired with the member at the studio's front desk. Send the number in exactly the format the studio pairs it, for example 10 decimal digits with leading zeros. Readers report the same card in different formats, so agree on the format with the studio before go-live.
qr The member's door ID: the uuid from the member list. For devices that identify the member themselves, such as a face terminal that matched a face you enrolled against the uuid, or a code you issued that carries the uuid.

The QR on the studio app's membership page does not open the door. That page shows a short-lived QR code for front-desk check-in, and this API refuses it. At the door, members use their wallet card (Apple Wallet, Google Wallet or the web card), and its barcode is sent as wallet.

Entry and exit

  • entry asks for a decision. Denied scans are recorded too.
  • exit is never refused: the answer is always granted, and the scan is recorded as an exit. If the code matches no member, data.user is null.

Response

Field Meaning
status granted or denied. This is the only field to act on.
success true exactly when status is granted.
message A sentence for people. It may be in the studio's language. Show it or log it, but never parse it.
data.log_id The number of this scan in the studio's access log. Quote it when you ask the studio about a scan.
data.user null when the code matches no member. Otherwise uuid, full_name and membership_status.
data.has_booking_today true if the member has a class booked today, false if not, or null if the studio does not take class bookings on EYalla. It does not affect the decision: a member does not need a booking to enter.
data.timestamp When the decision was made, in ISO 8601 with a UTC offset, for example +03:00.

Granted:

{
  "success": true,
  "status": "granted",
  "message": "Access granted.",
  "data": {
    "log_id": 5120,
    "user": {
      "uuid": "9b2f6c1e-3d4a-4f8e-a1b2-7c9d0e1f2a3b",
      "full_name": "Mona Al-Ali",
      "membership_status": "active"
    },
    "has_booking_today": true,
    "timestamp": "2026-10-08T07:02:11+03:00"
  }
}

Denied, a known member:

{
  "success": false,
  "status": "denied",
  "message": "Membership is frozen.",
  "data": {
    "log_id": 5121,
    "user": {
      "uuid": "4e8a2d70-91c5-4b3f-8e6d-0a1b2c3d4e5f",
      "full_name": "Yousef Hamad",
      "membership_status": "frozen"
    },
    "has_booking_today": false,
    "timestamp": "2026-10-08T07:03:40+03:00"
  }
}

Denied, a code that matches no member:

{
  "success": false,
  "status": "denied",
  "message": "Invalid access credential.",
  "data": {
    "log_id": 5122,
    "user": null,
    "has_booking_today": null,
    "timestamp": "2026-10-08T07:04:02+03:00"
  }
}

Why a scan is denied

message data.user Meaning
Invalid access credential. null The code matches no member, or the wallet card was revoked or replaced.
This account is inactive. membership_status: none The studio switched the member's account off or removed it.
Membership is frozen. membership_status: frozen The member's membership is paused.
No active membership found. The latest membership's status, e.g. expired, cancelled, pending No active membership covers today: it has ended, was cancelled, has not started yet, or is still waiting for payment.
Access denied. Varies Any other refusal, for example the studio's set-up is incomplete. Contact the studio.

In a verify answer, membership_status is the status recorded on the member's latest membership. It can still read active on a denial when that membership's end date has passed. Decide on status alone. The member list reports the door's own view.

Logging, time-outs and retries

  • Every call, granted or denied, is written to the studio's access log with your device_id and your IP address. Two identical scans make two entries; nothing is merged.
  • Do not resend a scan that received an answer.
  • If no answer arrives (time-out or network fault), keep the door shut and let the member scan again. A request time-out of 3 to 5 seconds suits a door. Open on failure only if the studio asks for that in writing.

An invalid request

A body that fails validation gets HTTP 422 with the field-by-field reasons. Send Accept: application/json so the answer is JSON.

{
  "message": "The selected credential type is invalid.",
  "errors": {
    "credential_type": ["The selected credential type is invalid."]
  }
}

The member list

GET /members

Lists the studio's members: everyone who holds, or has held, a membership there. Shop-only customers and removed accounts are not listed. The most recently changed members come first.

Query Default Meaning
page 1 The page to read.
per_page 50 Members per page, from 1 to 100.
status none (everyone) Only members with this membership_status. status=active lists exactly the members the door would let in now. With a filter, total and last_page count the filtered list.
search none Part of a member's name or e-mail address.

Request:

GET /api/v1/external/members?status=active&per_page=100&page=1

Response:

{
  "data": [
    {
      "uuid": "9b2f6c1e-3d4a-4f8e-a1b2-7c9d0e1f2a3b",
      "access_card_id": "0004512890",
      "name": "Mona Al-Ali",
      "membership_status": "active",
      "updated_at": "2026-10-07T18:22:05+03:00"
    }
  ],
  "meta": { "current_page": 1, "last_page": 3, "per_page": 100, "total": 241 }
}
Field Meaning
uuid The member's door ID. Every listed member has one, and it does not change. Use it as your key for the member.
access_card_id The access card paired with the member at the front desk, or null if none is paired.
name The member's name.
membership_status The door's view of the member now (see Membership status).
updated_at The last change to the member's account record, in ISO 8601. It is not a reliable signal that a membership started, ended or was paused.

One member

GET /members/{uuid}

Returns the same fields for one member, plus email and is_active (whether the studio has the account switched on). A uuid that is not on the member list returns HTTP 404:

{ "success": false, "message": "Member not found." }

Paging

Read pages in order from 1 to meta.last_page. A full pass over 250 members takes three requests at 100 per page.

Membership status

Value Meaning
active The door lets the member in now.
pending The membership has not started yet, or is waiting for payment.
expired The membership has ended.
frozen The membership is paused.
cancelled The membership was cancelled.
none The studio has switched the member's account off.

Keeping your copy up to date

  • Re-read the whole list on a schedule. A membership can start, end or be paused without updated_at changing, so comparing updated_at alone misses those changes. Every 15 minutes during opening hours suits most doors and costs only a few requests.
  • Match members by uuid. The list can change while you read it, so a member may appear on two pages, or move to a page you have already read. Remove a member from your copy only after a complete pass in which they did not appear.
  • Ask at the door whenever you can. A copy is only as fresh as your last pass. If your device decides from its copy while it is offline, it can let in someone whose membership ended after that pass. Whenever the device is online, the verify call is the answer.

Limits

Hourly limit per key

  • Each key may make a set number of requests per hour. The default is 1,000; the studio can set any figure from 10 to 100,000.
  • Every device that uses the same key shares the same allowance.
  • The hour starts with the key's first request and runs for 60 minutes.
  • Ask the studio for a limit above your busiest hour of scans plus your sync requests. For example, 400 scans and four 3-page syncs need about 412 requests.
  • The limit is set when the key is made. To change it, the studio issues a new key, which comes with a new secret.

Every response that passes authentication carries these headers:

Header Meaning
X-RateLimit-Limit Requests allowed per hour.
X-RateLimit-Remaining Requests left in the current hour.
X-RateLimit-Reset A Unix time one hour after this request. The current hour ends at or before it.

Over the limit, the answer is HTTP 429 with a Retry-After header in seconds:

{
  "success": false,
  "error": "rate_limited",
  "message": "API rate limit exceeded. Try again later.",
  "retry_after": 3600
}

retry_after is always the full hour. The limit may lift sooner, and a request after the hour ends is accepted.

Flood guard

More than 600 requests in one minute from one IP address are refused with HTTP 429 and Retry-After: 60, before the key is checked. No normal door comes close to this.


Errors

Errors use one shape. Act on the HTTP status and error; the message is for people.

{
  "success": false,
  "error": "key_inactive",
  "message": "This API key is inactive or expired."
}
HTTP error Meaning and what to do
401 missing_credentials A credential header is missing.
401 invalid_api_key Unknown key or wrong secret, or the studio has no access control set up. Check both values with the studio.
403 key_inactive The studio revoked the key, or it is past its expiry date.
403 missing_scope The key lacks the permission this call needs.
403 module_disabled The studio's Access Control add-on is not active. The studio must switch it back on.
404 (none) A single-member read for a uuid that is not on the member list.
422 (field errors) The request body is invalid.
429 rate_limited Over the hourly limit or the flood guard. Wait for Retry-After.
5xx (none) A temporary fault. At the door, keep it shut. For a sync, retry after 1, 2, then 4 minutes.

Examples

Replace <studio address>, the key and the secret with the values the studio sent you.

An entry with a wallet card:

curl -X POST "https://<studio address>/api/v1/external/access/verify" \
  -H "X-API-Key: ak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-API-Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"credential_type":"wallet","credential_id":"<code from the barcode>","action":"entry","device_id":"front-door-01"}'

An exit with an access card, naming the reader in a header:

curl -X POST "https://<studio address>/api/v1/external/access/verify" \
  -H "X-API-Key: ak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-API-Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Device-ID: exit-gate-02" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"credential_type":"card","credential_id":"0004512890","action":"exit"}'

The members the door lets in now, 100 at a time:

curl "https://<studio address>/api/v1/external/members?status=active&per_page=100&page=1" \
  -H "X-API-Key: ak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-API-Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Accept: application/json"

One member:

curl "https://<studio address>/api/v1/external/members/<uuid>" \
  -H "X-API-Key: ak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-API-Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Accept: application/json"

A door's decision, in outline:

// Called for every scan at an entry reader.
response = POST /access/verify { credential_type, credential_id, action: "entry", device_id }
                with a 3 to 5 second time-out

if no response, or HTTP status is not 200:
    keep the door shut          // show "please try again" or call staff
else if response.status == "granted":
    open the door
else:
    keep the door shut          // response.message may be shown to the member

Security and personal data

  • Use HTTPS only. Never put the key or the secret in a web address, a log file or a support ticket.
  • If a secret may have leaked, tell the studio. It can change the secret in seconds, and the old one stops working at once.
  • The member list is personal data: names, card numbers and, for single reads, e-mail addresses. Keep only what your device needs, protect it like any personal record, and delete it when the integration ends or the studio asks.
  • Do not treat a wallet code as a permanent identity. The studio can revoke or reissue a card, and its code changes when it does. Key your records on the member's uuid.

Go-live checklist

  • You have the base address, the key and the secret, and the secret came separately from the key.
  • Each reader sends its own device_id.
  • Your scanners read both QR codes and Code 128 barcodes.
  • The card-number format is agreed with the studio's front desk, and one test card paired at the desk is granted at your reader.
  • Tested with the studio: a member with an active membership is granted; a frozen and an expired member are denied; an unknown code is denied; an exit is recorded in the studio's access log.
  • The door opens only when status is granted, and stays shut after a time-out or an error.
  • The hourly limit covers your busiest hour of scans plus syncs.
  • If you keep a copy of the member list, it re-reads the whole list on a schedule.

Questions about a studio's set-up, keys or members go to that studio. Questions about this API go to developers@eyalla.com.