FuzeMetrix provisioning runbook
Internal runbook for IBA staff. Covers how we issue and manage FuzeMetrix
connector credentials (client-id + secret) and how to verify a partner’s
HMAC signing.
Partner-facing usage is documented on the FuzeMetrix integration page and the external API reference. A trimmed standalone partner guide is kept outside the repo for sending to third parties — never share this runbook with a partner.
Prerequisites
- An admin IBA account —
role_id = 1. The/op/*endpoints require admin (routesValidate([1])), and admin login is forced through TOTP (no OTP/email fallback). - An authenticator app (Google Authenticator / Authy) enrolled for that account, or its backup codes.
If the environment has no admin account (e.g. a fresh dev box), skip the API path and use the direct database shortcut below.
Environments
| Environment | Base URL | Notes |
|---|---|---|
| Dev | https://iba-dev-api-52i6l.ondigitalocean.app/api | Use for partner testing. |
| Production | https://api.tunnelflight.com/api | Live. |
Connector rows live in the environment’s own database. Credentials created on dev only work on dev — provision in each environment separately.
Step 1 — Get an admin JWT
Admin login is two calls: password, then TOTP.
1a. Login with username/password
curl --request POST \
--url 'https://iba-dev-api-52i6l.ondigitalocean.app/api/auth/login' \
--header 'content-type: application/json' \
--data '{"username":"ADMIN_USERNAME","password":"PASSWORD"}'No token is returned yet. The response is one of:
{"status":true,"requiresTotp":true,"member_id":ID}— TOTP already set up. Notemember_id, go to 1b.{"status":true,"requiresTotpSetup":true,"member_id":ID}— first time; do the one-time TOTP setup below first.
1b. Verify TOTP → returns the JWT
curl --request POST \
--url 'https://iba-dev-api-52i6l.ondigitalocean.app/api/auth/verify-totp' \
--header 'content-type: application/json' \
--header 'platform: ios' \
--data '{"username":"ADMIN_USERNAME","password":"PASSWORD","member_id":ID,"code":"123456"}'codeis the current 6-digit code from the authenticator (an 8-char backup code also works).- The
platform: iosheader makes the JWT appear in the response body astoken. Without it, the JWT is returned in thetokenresponse header (runcurl -ito see it).
Save that JWT — it’s the value for the token header in Steps 2–3.
Shortcut: logging into the dev admin dashboard in a browser performs this same TOTP flow; copy the
tokenfrom DevTools (Network request header or Application storage) instead of curling.
One-time TOTP setup (only if you got requiresTotpSetup)
# get QR + backup codes
curl --request POST \
--url 'https://iba-dev-api-52i6l.ondigitalocean.app/api/auth/totp/setup' \
--header 'content-type: application/json' \
--data '{"username":"ADMIN_USERNAME","password":"PASSWORD","member_id":ID}'
# -> scan qrCodeUrl into your authenticator app; store backupCodes safely
# confirm a code to finish setup (this also logs you in + returns the token)
curl --request POST \
--url 'https://iba-dev-api-52i6l.ondigitalocean.app/api/auth/totp/verify-setup' \
--header 'content-type: application/json' \
--header 'platform: ios' \
--data '{"username":"ADMIN_USERNAME","password":"PASSWORD","member_id":ID,"code":"123456"}'Step 2 — Create the partner’s connector user
# paste the JWT from Step 1 so the calls below can reuse it
export ADMIN_JWT='<paste the token from Step 1>'
curl --request POST \
--url 'https://iba-dev-api-52i6l.ondigitalocean.app/api/external/fuse-metrix/op' \
--header "token: $ADMIN_JWT" \
--header 'content-type: application/json' \
--data '{"email":"partner@example.com"}'The server auto-generates a clientId (32-char hex) and secret (base64). The
response is only {"status":true,"message":"User created successfully"} — it
does not return the credentials. Read them back in Step 3.
Step 3 — Read back the generated credentials
curl --request GET \
--url 'https://iba-dev-api-52i6l.ondigitalocean.app/api/external/fuse-metrix/op/' \
--header "token: $ADMIN_JWT"Returns all connector rows: each has id, email, clientId, secret,
created_at. Find the row for partner@example.com and copy its clientId and
secret. Get a single row by id instead with GET /api/external/fuse-metrix/op/:id.
Shortcut: provision directly in the database
When the environment has no admin account (so you can’t get a JWT), insert the
connector row directly — this is exactly what POST /op does under the hood
(see insertConnector in the connector model).
First generate a clientId + secret in the same format the code uses:
node -e 'const c=require("crypto");console.log("clientId =",c.randomBytes(16).toString("hex"));console.log("secret =",c.randomBytes(16).toString("base64"))'Then insert against the target environment’s database (replace the placeholders with the values above):
INSERT INTO fuse_metrix (email, created_by, secret, clientId, created_at)
VALUES ('partner@example.com', 1, 'GENERATED_SECRET', 'GENERATED_CLIENT_ID', UNIX_TIMESTAMP());The partner signs requests with that secret; hand them the clientId + secret
over a secure channel. To remove later:
DELETE FROM fuse_metrix WHERE clientId = 'GENERATED_CLIENT_ID';Step 4 — Hand off to the partner
Send the partner their clientId and secret over a secure channel (not
plaintext email/Slack). They sign each request as
token = HMAC_SHA256(secret, JSON.stringify(body)) (hex), with headers
client-id + token. Full details on the integration page.
Verifying a partner’s HMAC (debugging 403s)
If a partner reports 403 "Your token does not match", confirm the expected token
server-side with the admin-only helper:
curl --request POST \
--url 'https://iba-dev-api-52i6l.ondigitalocean.app/api/external/fuse-metrix/op/generate-token' \
--header "token: $ADMIN_JWT" \
--header 'client-id: PARTNER_CLIENT_ID' \
--header 'content-type: application/json' \
--data '{"member_id":40,"member_pin":"123456"}'Returns {"status":true,"message":"Token generated successfully","token":"..."}
— the HMAC the server expects for that exact body. Compare it to what the
partner computed. A mismatch is almost always whitespace or key-order between the
bytes they sign and the bytes they send (the server signs JSON.stringify(req.body),
which is compact).
Managing connector users
All require the token: $ADMIN_JWT header (the admin JWT from Step 1).
| Action | Call |
|---|---|
| List all | GET /api/external/fuse-metrix/op/ |
| Get one | GET /api/external/fuse-metrix/op/:id |
| Create | POST /api/external/fuse-metrix/op — body {"email":"..."} |
| Edit email | PUT /api/external/fuse-metrix/op — body {"id":N,"email":"..."} |
| Delete | DELETE /api/external/fuse-metrix/op/:id |
Gotchas
- JWT expires. If
/opstarts returning 401/403, re-run Step 1 for a fresh token. - Admin role required. Login works but
/op403s → the account isn’trole_id = 1. - Per-environment creds. Re-provision separately on prod when going live.
- Create returns no creds. Always read them back via
GET /op/(Step 3). - Rotating a secret. There’s no “regenerate secret” endpoint — delete the
connector and create a new one (the partner gets a new
client-id+secret).