Fingerspot Hub API — Agent Skills
A set of Agent Skills that teach AI coding agents (Claude Code, OpenCode, Codex, and other MCP-compatible tools) how to integrate with the Hub API. Each skill packages a complete, self-contained reference for one part of the API — endpoints, request/response shapes, supported device brands, and edge cases — so your agent can generate correct integration code without guessing.
# Install all skills
npx skills add royfingerspot/hub-api-skills --all
# Install a specific skill
npx skills add royfingerspot/hub-api-skills --skill hub-api-getting-started
# Preview available skills before installing
npx skills add royfingerspot/hub-api-skills --list
In Claude Code, every installed skill is also available as a slash command (e.g. /hub-api-webhook), so you can invoke it directly instead of waiting for auto-detection. Other agents (Codex, OpenCode, etc.) use the skill through normal auto-detection.
Source of truth:
openapi.yamlin the skills repo is the single source of truth for every endpoint, schema, and auth requirement. Always confirm brand support against the spec'sx-supported-brandsfield before calling an endpoint.
Skills Reference
hub-api-getting-started — Start here
Core integration patterns shared across every other skill: authentication, the async command flow, webhook handling, and error codes.
Authentication
Authorization: Bearer <your-api-token>
The same token is used both for API authentication and for signing/verifying webhook HMAC signatures.
Async command pattern
Most endpoints are asynchronous — they acknowledge immediately and deliver the actual result later via webhook.
- Send a request → server responds
200with an ack:{"referenceId": "your-tracking-id","status": "queued","deviceStatus": "connected","message": "command queued successfully","webhookUrls": ["https://your-webhook.com/callback"]} - Server POSTs the result to your webhook URL once the device responds:
{"referenceId": "your-tracking-id","status": "success","commandType": "setUser","data": { }}
- Correlate the two using
referenceId.
A handful of endpoints are synchronous and return data directly — no webhook involved:
GET /v1/{cloud_id}— device infoGET /v1/{cloud_id}/activity— device connectivity statusGET /v1/{cloud_id}/attendance— attendance logsPUT /v1/{cloud_id}/webhook— set webhook URL
Required headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <token> |
X-Reference-ID | Yes (async) | Your tracking ID, echoed back in the webhook callback |
X-Webhook-URL | No | Comma-separated extra webhook URLs (max 3) |
Content-Type | Yes (POST/PUT) | application/json |
Error codes
| Code | Meaning |
|---|---|
ERR_DEVICE_NOT_FOUND | Device not found in registry |
ERR_DEVICE_OFFLINE | Device not connected |
ERR_DEVICE_TIMEOUT | Command timeout (default 30 min) |
ERR_DEVICE_BUSY | Device processing another command |
ERR_INVALID_PARAMS | Invalid request body or query params |
ERR_DATE_RANGE | Invalid date range |
ERR_UNSUPPORTED | Device doesn't support this feature |
ERR_INTERNAL | Unexpected server error |
ERR_UNAUTHORIZED | Invalid or missing token |
If a command is sent to an offline device, the request still returns 200 with deviceStatus: "offline" — the command queues and executes once the device reconnects.
Supported brands across the API: vivo, vida, revo, vega, zkteco.
hub-api-users — User management
Get, create, update, and delete users on a device. All endpoints are async.
Headers: Authorization (yes), X-Reference-ID (yes), X-Webhook-URL (no)
| Endpoint | Method | Supported brands |
|---|---|---|
/v1/{cloud_id}/users | GET — all users | vida, vivo |
/v1/{cloud_id}/user-ids | GET — lightweight ID/PIN list | revo, vega, vivo, vida, zkteco |
/v1/{cloud_id}/users/{id} | GET — single user with credentials | vivo, vida, zkteco |
/v1/{cloud_id}/users | POST — create/update user | vivo, vida, revo, vega, zkteco |
/v1/{cloud_id}/users/{id} | DELETE | vida, vivo, zkteco |
Create/update request body:
{
"employeeNo": "101",
"name": "John",
"userType": "admin",
"password": "123456",
"fingerprints": [{ "fingerNo": 1, "fingerData": "base64..." }],
"cards": [{ "cardNo": "12345678" }]
}
userType accepts normal, admin, visitor on most brands — Revo additionally supports user, operator, manager, supervisor.
Note: getAllUsers only works on vida and vivo. For other brands, use user-ids instead.
hub-api-credentials — Fingerprint, face, card, password, QR code
Manage biometric and non-biometric credentials. All endpoints are async.
Headers: Authorization (yes), X-Reference-ID (yes), X-Webhook-URL (no)
Fingerprint
| Endpoint | Method | Notes |
|---|---|---|
/v1/{cloud_id}/users/{id}/finger | GET | All fingerprints for a user |
/v1/{cloud_id}/users/{id}/finger/{fingerNo} | GET | One fingerprint (slot 1–10) |
/v1/{cloud_id}/users/{id}/finger | PUT | Replace all fingerprints (bulk) |
/v1/{cloud_id}/users/{id}/finger/{fingerNo} | PUT | Set one fingerprint |
/v1/{cloud_id}/users/{id}/finger | DELETE | Remove all |
/v1/{cloud_id}/users/{id}/finger/{fingerNo} | DELETE | Remove one |
Face
| Endpoint | Method |
|---|---|
/v1/{cloud_id}/users/{id}/face | GET / PUT / DELETE |
Supported brands: vida, vivo.
Card
| Endpoint | Method |
|---|---|
/v1/{cloud_id}/users/{id}/card | GET |
/v1/{cloud_id}/users/{id}/card | PUT — body: { "cardNo": "...", "cardType": "normalCard" } |
/v1/{cloud_id}/users/{id}/card/{cardNo} | DELETE |
cardType defaults to normalCard; hijackCard is also supported.
Password
GET / PUT /v1/{cloud_id}/users/{id}/password — body: { "password": "newpassword123" }
QR Code
| Endpoint | Method |
|---|---|
/v1/{cloud_id}/qrcode | GET — all QR codes on device |
/v1/{cloud_id}/qrcode/{recno} | GET / DELETE |
/v1/{cloud_id}/users/{id}/qrcode | GET / POST / PUT / DELETE |
Fingerprint templates and face data are base64-encoded; format for face data varies by brand.
hub-api-attendance — Attendance log queries
The only fully synchronous skill — returns data directly, no webhook.
Headers: Authorization (yes), X-Reference-ID (no, but recommended for tracking)
Endpoint
GET /v1/{cloud_id}/attendance
Query params
| Param | Type | Required | Notes |
|---|---|---|---|
startDate | string | yes | YYYY-MM-DD |
endDate | string | yes | YYYY-MM-DD |
page | int | no | Default 1 |
limit | int | no | Default 100, max 100 |
Supported brands: vivo, vida, revo, vega, zkteco.
Response
{
"records": [
{
"employeeNo": "101",
"name": "John",
"cardNo": "12345678",
"eventTime": "2026-06-15T08:00:00+07:00",
"verifyMode": "fingerprint",
"attendanceStatus": "check_in",
"userType": "admin",
"serialNumber": "ABC123",
"cloudId": "GQ5778408"
}
],
"total": 50,
"page": 1,
"limit": 100
}
verifyMode: fingerprint, password, card, face, vein. attendanceStatus: check_in, check_out.
hub-api-device — Device info, time, reboot, timezone, capture
A mix of sync and async endpoints — check each one individually.
Headers: Authorization (yes), X-Reference-ID (yes, async endpoints), X-Webhook-URL (no)
Device info & status (sync)
| Endpoint | Returns |
|---|---|
GET /v1/{cloud_id} | brand, serial, capabilities |
GET /v1/{cloud_id}/activity | connectivity status, last-seen timestamp |
Time
PUT /v1/{cloud_id}/time — body: { "dateTime": "...", "timezone": "Asia/Jakarta", "mode": "ntp" }. At least one field required. Timezone accepts IANA names or ISO 8601 offsets.
Reboot
POST /v1/{cloud_id}/reboot — no body. Vida only.
Timezone
GET / PUT /v1/{cloud_id}/timezone, plus PUT /v1/{cloud_id}/timezone/lock to lock/unlock physical-device changes. Supported: vivo, vida, zkteco.
Valid date
GET / PUT /v1/{cloud_id}/users/{id}/valid — sets a user's active period:
{
"employeeNo": "101",
"beginDate": "2026-01-01",
"endDate": "2026-12-31",
"weekTimeZone": [1, 1, 1, 1, 1, 1, 1]
}
weekTimeZone is 7 values (Mon–Sun), 1 = active, 0 = inactive.
Live capture & enrollment
| Endpoint | Purpose | Supported brands |
|---|---|---|
POST /v1/{cloud_id}/capture/finger | Trigger live fingerprint capture | vivo, vida, revo, vega, zkteco |
POST /v1/{cloud_id}/capture/card | Trigger live card capture | vivo, vida, revo, vega, zkteco |
POST /v1/{cloud_id}/enroll/finger | Device-initiated finger enrollment | revo, vega |
POST /v1/{cloud_id}/enroll/face | Device-initiated face enrollment | revo, vega |
POST /v1/{cloud_id}/enroll/vein | Device-initiated vein enrollment | revo, vega |
Enrollment commands put the device into enrollment mode — the user must physically interact with the device afterward.
hub-api-door — Door open/close/status
Async endpoints, currently Vida-only.
Headers: Authorization (yes), X-Reference-ID (yes), X-Webhook-URL (no)
| Endpoint | Method |
|---|---|
/v1/{cloud_id}/door/open | POST — body optional: { "channel": 1 } |
/v1/{cloud_id}/door/close | POST — body optional: { "channel": 1 } |
/v1/{cloud_id}/door/status | GET |
channel defaults to 1 and is only relevant for devices with multiple door controllers. Door open/close/alarm events are also pushed as realtime webhook events (see hub-api-webhook).
hub-api-webhook — Configuration, signature verification, realtime events
Configuration
PUT /v1/{cloud_id}/webhook (sync) — sets the device's default webhook URL:
{ "url": "https://your-server.com/webhook" }
POST /v1/{cloud_id}/webhook/test (sync) — checks whether the configured URL is reachable.
Webhook URL resolution order: X-Webhook-URL header → per-device default → V2_DEFAULT_WEBHOOK_URL env var fallback.
Signature verification
Every webhook includes HMAC-SHA256 signatures — always verify before trusting a callback.
| Header | Description |
|---|---|
X-Hub-API-Timestamp | Unix timestamp (seconds, UTC) |
X-Hub-API-Signature-256 | sha256=<hex-digest> |
Steps: reject if now - timestamp > 300s → compute HMAC-SHA256(body, api_token) → compare using constant-time comparison → accept only on match.
const crypto = require('crypto');
function verifyWebhook(body, timestamp, signature, secret) {
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) return false;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Official SDKs (PHP) expose a ready-made verifier: new WebhookVerifier($apiToken)->verify($body, $_SERVER, 300).
Retry behavior: failed deliveries retry with exponential backoff (1s, 2s, 4s, 8s, 16s across 5 attempts), then stop — no dead-letter queue.
Realtime events
Device-initiated pushes, independent of any command you sent:
| Event | Description |
|---|---|
attendance | User scanned (check-in/out) |
verificationFailed | Verification attempt rejected |
doorEvent | Door state changed |
doorbell | Doorbell pressed |
tamper | Physical tamper detected |
alarm | Device alarm triggered |
exception | Device exception |
operation | Device operation event |
userChange | User data changed directly on device |
deviceActivity | Connectivity status changed (online/offline) |
Every event payload includes event and cloudId for routing, and is signed the same way as command-result webhooks.
Next Steps
- Read the API Reference for the full endpoint catalog
- Check Getting Started for API authentication setup
- Explore Webhook Signature for the platform-level signing guide
- Review Rate Limiting for quota policies