Skip to content

Pairing & devices

Pairing links a user (on the backend side) to a device running the Bloonio Authenticator app. It happens in two stages: the backend prepares the pairing (HMAC #1 signed) and receives a short pairing_proof; the device exchanges that proof for a long-lived device_session_token.

1. the user scans the QR (backend's existing pairing handler)
2. backend ── HMAC #1 ─► POST /api/v1/relay/prepare-pairing
← { pairing_proof (JWT ~5 min), expires_in }
3. the backend returns pairing_proof to the device with its usual pairing payload
4. device ── Bearer pairing_proof ─► POST /api/v1/device/register-token
← { device_session_token (JWT ~30 days), pairing }
5. the device persists device_session_token and uses it for refresh-token / unpair
  1. The backend prepares the pairing (HMAC #1).

    After authenticating the user in your QR pairing flow, call prepare-pairing. At least one of user_email / user_phone is required; user_socket_hash, backend_user_id, and display_name are mandatory.

    Fenêtre de terminal
    curl $BASE_URL/api/v1/relay/prepare-pairing \
    -X POST \
    -H "X-Bloonio-Tenant-Id: tnt_..." \
    -H "X-Bloonio-Timestamp: 1718966400000" \
    -H "X-Bloonio-Signature: <hex_hmac_sha256>" \
    -H "Content-Type: application/json" \
    -d '{
    "user_socket_hash": "9f86d081884c7d659a2feaa0c55ad015",
    "backend_user_id": "507f1f77bcf86cd799439011",
    "user_email": "jane@example.com",
    "first_name": "Jane",
    "last_name": "Doe",
    "display_name": "Example",
    "display_logo_url": "https://example.com/logo.png"
    }'
    {
    "success": true,
    "status_code": 201,
    "message": "Pairing prepared. Forward pairing_proof to the device.",
    "data": { "pairing_proof": "eyJhbGciOi...", "expires_in": 300 }
    }
  2. The backend forwards the pairing_proof to the device.

    Return it to the device alongside your existing pairing payload. The proof is a JWT valid for ~5 minutes (PAIRING_PROOF_TTL_SECONDS, 300 s by default) — a single transfer window.

  3. The device registers its FCM token (Bearer pairing_proof).

    The device exchanges the proof for a long-lived device_session_token and receives the pairing’s public info. platform is ios or android.

    Fenêtre de terminal
    curl $BASE_URL/api/v1/device/register-token \
    -X POST \
    -H "Authorization: Bearer eyJhbGciOi..." \
    -H "Content-Type: application/json" \
    -d '{
    "fcm_token": "fcm-token-value-here",
    "platform": "android",
    "app_version": "1.4.0",
    "os_version": "14"
    }'
    {
    "success": true,
    "status_code": 201,
    "message": "Device token registered.",
    "data": {
    "device_session_token": "eyJhbGciOi...",
    "expires_in": 2592000,
    "pairing": {
    "tenant_id": "tnt_...",
    "user_socket_hash": "9f86d081884c7d659a2feaa0c55ad015",
    "display_name": "Example",
    "display_logo_url": "https://example.com/logo.png",
    "created_at": "2026-06-21T...",
    "last_seen_at": null
    }
    }
    }
  4. The device persists the device_session_token.

    This JWT (~30 days, DEVICE_SESSION_TOKEN_TTL_SECONDS, 2,592,000 s by default) authenticates the subsequent calls: refreshing the FCM token and unpairing.

When the FCM token rotates locally on the device, call refresh-token with the device_session_token.

Fenêtre de terminal
curl $BASE_URL/api/v1/device/refresh-token \
-X POST \
-H "Authorization: Bearer eyJhbGciOi..." \
-H "Content-Type: application/json" \
-d '{ "new_fcm_token": "new-fcm-token-value" }'
{ "success": true, "status_code": 200, "message": "Token refreshed", "data": null }

Two revocation paths, depending on which side acts.

The user removes the account from the device. Authenticated by the device_session_token. No body.

Fenêtre de terminal
curl $BASE_URL/api/v1/device/unpair \
-X POST \
-H "Authorization: Bearer eyJhbGciOi..."
{ "success": true, "status_code": 200, "message": "Device unpaired", "data": null }