KIOSK API
Machine-to-machine API for MagNicas kiosk devices. Look up approved visitor passes, read a visit by id, register walk-in visitors, and pull property reference data (visitor categories, members, spaces and visit hours) using a service-account credential.
Base URL
All KIOSK endpoints live under a versioned path. Add /api/v1/kiosk to the API origin:
https://staging-api.magnicas.app/api/v1/kiosk
For example, a full request URL looks like:
POST https://staging-api.magnicas.app/api/v1/kiosk/visits/lookup
Authentication
KIOSK is a machine-to-machine API authenticated by a service credential issued to a service account. Send the credential as a bearer token or an API key header:
Authorization: Bearer kiosk_xxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-API-Key: kiosk_xxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
| Item | Notes |
|---|---|
| Credential format | kiosk_<prefix>.<secret> — the raw key is shown once when issued and is never retrievable again. |
| Required scope | kiosk — the service account must carry this scope or the API returns INVALID_SCOPE. |
| Rotation | Credentials can be rotated; previously issued credentials remain valid for a short overlap window, then expire. |
| Revocation | The property manager revokes a kiosk's credentials in the management portal (the service account's Edit page → Revoke all credentials). A revoked credential stops working at once and every call returns 401 CREDENTIAL_REVOKED; issue a new one to bring the kiosk back. Revoke straight away if a key may have leaked. |
| Rate limit | Each credential may make 120 calls a minute. Past that the API returns 429 RATE_LIMITED with a Retry-After header giving the seconds to wait. A kiosk in normal use stays far below it; two kiosks with their own credentials each have their own allowance. |
Missing or invalid credentials return 401 UNAUTHENTICATED:
{
"meta": {
"code": 401,
"status": "error",
"error_code": "UNAUTHENTICATED",
"message": "Missing or invalid service credential.",
"message_ms": "Kelayakan perkhidmatan tiada atau tidak sah."
},
"data": null
}
Look up a visit
https://staging-api.magnicas.app/api/v1/kiosk/visits/lookup
Resolves the QR scanned by the kiosk and returns the matching visit record for display. The scanned value is either the badge's own QR content (a plain string stored on the visit) or an encrypted visitor pass, which MagNicas decrypts.
Request
| Field | Type | Required | Description |
|---|---|---|---|
qrcode |
string | yes | The scanned value, passed through verbatim — the kiosk must not parse, trim or modify it. Either the badge QR content (plain string, matched as scanned) or the encrypted visitor pass payload (decrypted by MagNicas and matched on the visit's Vi-Link slug and OTP). |
curl -X POST https://staging-api.magnicas.app/api/v1/kiosk/visits/lookup \
-H "Authorization: Bearer kiosk_xxx" \
-H "Content-Type: application/json" \
-d '{"qrcode": "<scanned value, verbatim>"}'
QR_PAYLOAD_EXPIRED; the visitor
opens the Vi-Link again for a fresh QR. Set to 0, the QR is
static — it never changes, so a printed or screenshotted pass works for
the whole visit. Pilots usually run with a static QR. Either way the kiosk sends the
scanned value unchanged and never decodes it.
Response
{
"meta": {
"code": 200,
"status": "success",
"message": "QR Code Visitor Retrieval Successful."
},
"data": {
"hashid": "vLx9k2mQ1a",
"name": "Test Visitor",
"national_id": "800101-01-1234",
"host": "Host User",
"purpose": "Meeting",
"category": "guest",
"category_display": "Guest",
"validity_type": "day",
"validity_type_display": "Multi-Day",
"validity": {
"from": "01 Sep 2026 08:00 am",
"until": "03 Sep 2026 06:00 pm",
"label": "01 Sep 2026 08:00 AM - 03 Sep 2026 06:00 PM"
},
"multi_visit_hour": null,
"spaces": [
{
"space_name": "Space A",
"unit_no": "A-01",
"block": "A",
"level": "1",
"offerable": true
}
],
"face_picture_enrolled": false,
"status": "approved"
}
}
Both shapes resolve to the visit record above, and the visit must belong to the
service account's property. The lookup validates the value's expiry, the visit
approval status, blacklist state, and the validity window, and refuses a visit
more than 30 minutes before it starts (VISIT_NOT_STARTED). Failures return the
matching error from the error catalog, for example
QR_PAYLOAD_EXPIRED (“Your pass has expired. Please refresh your pass.”).
Get a visit
https://staging-api.magnicas.app/api/v1/kiosk/visits/{hashid}
Returns one visit of the service account's property by the identifier the lookup
returned ({hashid} is data.hashid from the lookup
response). The response carries the same visit record as
Look up a visit, so a kiosk that already resolved a visit can
re-read it — for example to refresh the status on screen — without presenting the
pass again.
curl https://staging-api.magnicas.app/api/v1/kiosk/visits/vLx9k2mQ1a \
-H "Authorization: Bearer kiosk_xxx"
Response shape matches the visit object shown under Look up a visit.
status. Property scope is still enforced — a visit of another property
returns 404 RESOURCE_NOT_FOUND.
Register a walk-in
https://staging-api.magnicas.app/api/v1/kiosk/visits/walk-in
Creates a walk-in visitor registration, binds an access card, checks the visitor in, issues the visitor's QR pass, provisions access, and attaches the document checklist in a single call. The card must not already be bound to an active visit.
Request
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Visitor full name (max 255). |
national_id |
string | yes | NRIC / national ID (max 255). |
category |
string | yes | Slug from List visitor categories. The five shared categories are guest, contractor, transporter, delivery, other; a property may hide one, and may add its own. |
member_hashid |
string | yes | Hashid of the member (host) this visitor is visiting. |
card_number |
string | yes | Access card number to bind (max 255). |
space_ids |
array / JSON | property rules | The spaces the visitor may access, as a JSON array (or a JSON-encoded string of one). Each entry is a space hashid from GET /spaces or its numeric id. Only visitor-offerable spaces are allowed. Required unless the property has spaces switched off. |
validity_type |
string | yes | hour — a single day, entered inside the window below; day — a multi-day pass. |
validity |
integer | yes | 1 to 7. For a day pass, the number of days counted from today; for an hour pass the window comes from the times below, so send 1. More than 7 days is refused with VALIDATION_ERROR — longer visits are pre-registered by the host instead. |
validity_start_time / validity_end_time |
string HH:MM |
if hour | Window the visitor may enter on that day, in the property's time zone. Required when validity_type is hour. |
visit_hour_id |
integer | no | For a multi-day (day) pass on a property that keeps visit hours: the id from List visit hours. The hour's window then applies to every day of the pass. Ignored unless the property's visit hours are enabled. |
pdpa_consent |
boolean | yes | Must be true — the visitor accepted the PDPA consent. |
email |
string | no | Valid email address. |
phone_no |
string | property rules | Contact number (max 255). When the property requires it, it must be + followed by digits. |
purpose_of_visits |
string | property rules | Purpose of the visit (max 255). |
face_photo |
string | property rules | Face photo captured by the kiosk: a base64 string (a data:image/jpeg;base64,… URL is fine) in a JSON request, or an image file (jpeg, png or jpg, max 2048 KB) in a multipart request. Both work whatever the property's face setting. |
vehicle_number |
string | property rules | Vehicle plate number (max 255). |
note |
string | no | Free-form note. |
Property-driven requirements
A property decides which of the optional fields its kiosk must collect, so the same
call may be accepted at one property and rejected at the next. The first unsatisfied
rule comes back as 422 VALIDATION_ERROR, naming the field.
| Property setting | Effect on this request |
|---|---|
visitor_security_spaces_required |
space_ids required; only a property with spaces switched off may be registered without spaces. |
visitor_security_phone_required (property default: on) |
phone_no required, and it must match + followed by digits. |
visitor_security_national_id_required |
national_id required. |
visitor_security_vehicle_no_required or enable_lpr = mandatory |
vehicle_number required. |
visitor_security_purpose_visits_required |
purpose_of_visits required. |
enable_face_recognition = mandatory |
face_photo required (base64 or image file, as above). |
curl -X POST https://staging-api.magnicas.app/api/v1/kiosk/visits/walk-in \
-H "Authorization: Bearer kiosk_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Walk In Visitor",
"national_id": "900101-01-5678",
"category": "guest",
"purpose_of_visits": "Site visit",
"member_hashid": "mP4x9Kd2Lq",
"card_number": "CARD-001",
"space_ids": ["sA7kQ2mLx9", "sB3nW8pRt4"],
"phone_no": "+60123456789",
"validity_type": "day",
"validity": 1,
"pdpa_consent": true
}'
Response
{
"meta": {
"code": 200,
"status": "success",
"message": "Visitor Registration Successful."
},
"data": {
"hashid": "vLx9k2mQ1a",
"status": "checkin",
"check_in_at": "2026-09-02T10:15:00+08:00",
"host": "Verified Staff",
"card_number": "CARD-001",
"spaces": ["Space A", "Space B"],
"face_picture_enrolled": true
}
}
409 DUPLICATE_ACTIVE_REGISTRATION.
List visitor categories
https://staging-api.magnicas.app/api/v1/kiosk/visitor-categories
The categories this property may register a visitor with, for the kiosk's category selector.
No query parameters.
Response
{
"meta": {
"code": 200,
"status": "success",
"message": "Visitor Category Retrieval Successful."
},
"data": {
"categories": [
{ "slug": "contractor", "name": "Contractor", "photo": "https://staging-api.magnicas.app/category-icon/contractor.png" },
{ "slug": "delivery", "name": "Delivery", "photo": "https://staging-api.magnicas.app/category-icon/delivery.png" },
{ "slug": "guest", "name": "Guest", "photo": "https://staging-api.magnicas.app/category-icon/family.png" },
{ "slug": "other", "name": "Other", "photo": "https://staging-api.magnicas.app/category-icon/other.png" },
{ "slug": "transporter", "name": "Transporter", "photo": "https://staging-api.magnicas.app/category-icon/transport.png" }
]
}
}
| Field | Notes |
|---|---|
slug |
The value to send in the category field of Register walk-in. |
name |
The label the category selector shows. |
photo |
The image the selector shows for the category. A shared category uses the icon the registration form shows; a category the property maintains uses the image it uploaded, and is null when it has no image — fall back to a local placeholder then. |
The list is the platform's visible shared categories plus the ones the property
maintains itself — so the selector offers exactly what a walk-in registration
accepts. A slug that is not in this list is rejected with
VALIDATION_ERROR.
List members
https://staging-api.magnicas.app/api/v1/kiosk/members
Paginated list of approved, active members of the service account's property.
Query parameters
| Parameter | Type | Description |
|---|---|---|
per_page |
integer | Items per page, 1–100 (default 15). |
search |
string | Case-insensitive name search (max 255). |
Response
{
"meta": {
"code": 200,
"status": "success",
"message": "Members"
},
"data": [
{
"hashid": "mP4x9Kd2Lq",
"name": "Member One",
"national_id": "800101-01-1234",
"role": "Owner",
"unit": {
"space_name": "Unit 1",
"unit_no": "U-01",
"block": "A",
"level": "1"
},
"status": "approved"
}
],
"pagination": {
"total": 1,
"count": 1,
"per_page": 15,
"current_page": 1,
"total_pages": 1,
"links": {
"first_page": "https://api.example.com/api/v1/kiosk/members?page=1",
"last_page": "https://api.example.com/api/v1/kiosk/members?page=1",
"next_page": null,
"prev_page": null
}
}
}
Get a member
https://staging-api.magnicas.app/api/v1/kiosk/members/{hashid}
Returns a single approved, active member by hashid. Returns
404 RESOURCE_NOT_FOUND if the member does not exist or does not belong
to the service account's property.
curl https://staging-api.magnicas.app/api/v1/kiosk/members/mP4x9Kd2Lq \
-H "Authorization: Bearer kiosk_xxx"
Response shape matches the member object shown under List members.
List spaces
https://staging-api.magnicas.app/api/v1/kiosk/spaces
Paginated list of active spaces (units) at the service account's property.
Query parameters
| Parameter | Type | Description |
|---|---|---|
per_page |
integer | Items per page, 1–100 (default 15). |
search |
string | Case-insensitive name search (max 255). |
offerable |
boolean | Filter by whether the space is offerable to visitors. |
Response
{
"meta": {
"code": 200,
"status": "success",
"message": "Unit Properties"
},
"data": [
{
"id": 1,
"hashid": "sP8aR3tY5u",
"space_name": "Space A",
"unit_no": "A-01",
"block": "A",
"level": "1",
"offerable": true,
"is_active": true
}
],
"pagination": {
"total": 1,
"count": 1,
"per_page": 15,
"current_page": 1,
"total_pages": 1,
"links": {
"first_page": "https://api.example.com/api/v1/kiosk/spaces?page=1",
"last_page": "https://api.example.com/api/v1/kiosk/spaces?page=1",
"next_page": null,
"prev_page": null
}
}
}
Get a space
https://staging-api.magnicas.app/api/v1/kiosk/spaces/{hashid}
Returns a single active space by hashid. Returns 404 RESOURCE_NOT_FOUND
if the space does not exist or does not belong to the service account's property.
curl https://staging-api.magnicas.app/api/v1/kiosk/spaces/sP8aR3tY5u \
-H "Authorization: Bearer kiosk_xxx"
Response shape matches the space object shown under List spaces.
List visit hours
https://staging-api.magnicas.app/api/v1/kiosk/visit-hours
The visiting windows the property maintains, for the kiosk's hour selector.
No query parameters.
Response
{
"meta": {
"code": 200,
"status": "success",
"message": "Visit Hour Retrieval Successful."
},
"data": {
"enabled": true,
"visit_hours": [
{
"id": 1,
"title": "Weekday access",
"description": "Visits between 9am and 6pm on weekdays.",
"display_time": "09:00 am - 06:00 pm",
"start_time": "09:00:00",
"end_time": "18:00:00",
"start_time_wttz": "01:00:00+00",
"end_time_wttz": "10:00:00+00",
"is_default": true,
"is_active": true,
"allowed_days": ["mon", "tue", "wed", "thu", "fri"]
}
]
}
}
| Field | Notes |
|---|---|
enabled |
Mirrors the property's visit-hours setting. When false the property does not apply visit hours on its registration forms — do not offer the hour selector, even though the maintained hours are still listed. |
id |
The visit hour's number, used with GET /visit-hours/{id}. |
title / description |
The property's own label for the window; the description may be null. |
display_time |
Ready-to-show reading, e.g. 09:00 am - 06:00 pm. |
start_time / end_time |
The same window as HH:MM:SS on the property's clock — the authoritative values. |
start_time_wttz / end_time_wttz |
The window as stored (UTC), for consumers that compute with it. |
is_default |
The window the property marks as its default. |
allowed_days |
Weekday codes (mon, tue, wed, thu, fri, sat, sun) the window applies to, Monday first. An empty list means the hour does not restrict which days it may be used on. |
visit_hours list.
Get a visit hour
https://staging-api.magnicas.app/api/v1/kiosk/visit-hours/{id}
Returns one visit hour with the window each weekday of its schedule opens and
closes. Unknown, inactive, or foreign visit hours return
404 RESOURCE_NOT_FOUND.
curl https://staging-api.magnicas.app/api/v1/kiosk/visit-hours/1 \
-H "Authorization: Bearer kiosk_xxx"
Response
{
"meta": {
"code": 200,
"status": "success",
"message": "Visit Hour Detail Retrieval Successful."
},
"data": {
"id": 1,
"title": "Weekday access",
"description": "Visits between 9am and 6pm on weekdays.",
"display_time": "09:00 am - 06:00 pm",
"start_time": "09:00:00",
"end_time": "18:00:00",
"start_time_wttz": "01:00:00+00",
"end_time_wttz": "10:00:00+00",
"is_default": true,
"is_active": true,
"allowed_days": ["mon", "tue", "wed", "thu", "fri"],
"days": [
{
"day": "mon",
"day_number": 1,
"label": "Monday",
"allowed": true,
"start_time": "09:00:00",
"end_time": "18:00:00",
"display_time": "09:00 am - 06:00 pm"
},
{
"day": "sat",
"day_number": 6,
"label": "Saturday",
"allowed": false,
"start_time": null,
"end_time": null,
"display_time": null
}
],
"created_at": "2026-09-01T18:00:00+08:00",
"updated_at": "2026-09-01T18:00:00+08:00"
}
}
The example above shows Monday and Saturday; days always carries all
seven weekdays, Monday to Sunday, so the kiosk
can render the whole week without filling gaps itself. day_number is
the ISO-8601 weekday (1 = Monday). A day the hour does not open has
allowed: false and null times; a day the hour opens but keeps no window
of its own follows the hour's overall start_time /
end_time.
Error catalog
Every error a KIOSK endpoint may return comes from the versioned catalog below (version 1.2). Codes never change meaning within a version, and new codes are only added.
Error response shape
{
"meta": {
"code": 422,
"status": "error",
"error_code": "VALIDATION_ERROR",
"message": "The qrcode field is required.",
"message_ms": "Permintaan tidak sah."
},
"data": null
}
Catalog
| Code | HTTP | English | Bahasa Malaysia |
|---|---|---|---|
UNAUTHENTICATED |
401 | Missing or invalid service credential. | Kelayakan perkhidmatan tiada atau tidak sah. |
CREDENTIAL_REVOKED |
401 | Service credential has been revoked. | Kelayakan perkhidmatan telah dibatalkan. |
CREDENTIAL_EXPIRED |
401 | Service credential has expired. | Kelayakan perkhidmatan telah tamat tempoh. |
INVALID_SCOPE |
403 | Service account does not have the required scope. | Akaun perkhidmatan tidak mempunyai skop yang diperlukan. |
SERVICE_ACCOUNT_INACTIVE |
403 | Service account is inactive. | Akaun perkhidmatan tidak aktif. |
INVALID_QR_PAYLOAD |
422 | The QR payload could not be read. | Kandungan QR tidak dapat dibaca. |
QR_PAYLOAD_EXPIRED |
422 | Your pass has expired. Please refresh your pass. | Pas anda telah tamat tempoh. Sila muat semula pas anda. |
IDENTITY_MISMATCH |
422 | Visitor identity does not match the record. | Identiti pelawat tidak sepadan dengan rekod. |
EXPIRED_VALIDITY |
422 | The visit is outside its validity window. | Lawatan berada di luar tempoh sah. |
VISIT_NOT_STARTED |
422 | The visit has not started yet. Please come back closer to your visit time. | Lawatan belum bermula. Sila datang semula hampir dengan masa lawatan anda. |
VISIT_NOT_APPROVED |
403 | The visit has not been approved. | Lawatan belum diluluskan. |
VISITOR_BLACKLISTED |
403 | Visitor is blacklisted. Please proceed to the security desk. | Pelawat disenarai hitam. Sila ke meja keselamatan. |
DUPLICATE_ACTIVE_REGISTRATION |
409 | An active registration already exists for this visitor. | Pendaftaran aktif untuk pelawat ini sudah wujud. |
CARD_BINDING_FAILED |
422 | The card could not be bound. | Kad tidak dapat dipautkan. |
CARD_STOCK_EMPTY |
409 | No cards are available. | Tiada kad tersedia. |
DEVICE_FAULT |
500 | A kiosk device fault occurred. Please contact the security desk. | Berlaku kerosakan peranti kiosk. Sila hubungi meja keselamatan. |
VALIDATION_ERROR |
422 | The request is invalid. | Permintaan tidak sah. |
RATE_LIMITED |
429 | Too many requests. Please try again shortly. | Terlalu banyak permintaan. Sila cuba sebentar lagi. |
RESOURCE_NOT_FOUND |
404 | The requested resource was not found. | Sumber yang diminta tidak ditemui. |
INTERNAL_ERROR |
500 | An unexpected error occurred. | Berlaku ralat yang tidak dijangka. |
Audit trail
Every successful or failed KIOSK call is recorded in the API audit trail, including
the service account, action (lookup, visit-detail,
walk-in, card binding), the visitor involved, and the outcome. Audit
entries are retained per the configured retention policy.