MagNicas / API Docs

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.

Scope: every KIOSK endpoint is scoped to the property the service account belongs to. A kiosk can only read or write data for its own property.
Door access: the kiosk talks only to this API and never calls the MagEtegra Web API. MagNicas grants a visitor's door access in MagEtegra when the visit is checked in (a pass scanned on its day, or a walk-in), and removes it when the visit ends. A kiosk needs no MagEtegra address or 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

POST 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>"}'
Rotating or static QR: each property decides whether the visitor's Vi-Link QR changes over time (the QR Code Auto-Refresh Interval, in minutes, in the property settings). With a refresh interval set, a pass carries its own expiry and an old screenshot is refused with 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

GET 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.

Plain read: unlike the QR lookup this endpoint does not re-apply the pass expiry, approval, blacklist or validity-window checks, so a visit that has since checked out, expired or overstayed is still returned with its current status. Property scope is still enforced — a visit of another property returns 404 RESOURCE_NOT_FOUND.

Register a walk-in

POST 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
  }
}
A walk-in is atomic: if registration, card binding, or check-in fails, the request returns an error and no visitor record is created. Duplicate active registrations return 409 DUPLICATE_ACTIVE_REGISTRATION.

List visitor categories

GET 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.

Categories are administered per property in the management portal. Hiding one affects new registrations only — visits that already carry it keep showing it in the visit record.

List members

GET 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

GET 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

GET 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

GET 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

GET 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.
Only active visit hours of the service account's property are returned, in the order the property displays them. A property that maintains no visit hours returns an empty visit_hours list.

Get a visit hour

GET 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.