Skip to Content
IntegrationsFuzemetrixProvisioning runbook

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

EnvironmentBase URLNotes
Devhttps://iba-dev-api-52i6l.ondigitalocean.app/apiUse for partner testing.
Productionhttps://api.tunnelflight.com/apiLive.

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. Note member_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"}'
  • code is the current 6-digit code from the authenticator (an 8-char backup code also works).
  • The platform: ios header makes the JWT appear in the response body as token. Without it, the JWT is returned in the token response header (run curl -i to 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 token from 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).

ActionCall
List allGET /api/external/fuse-metrix/op/
Get oneGET /api/external/fuse-metrix/op/:id
CreatePOST /api/external/fuse-metrix/op — body {"email":"..."}
Edit emailPUT /api/external/fuse-metrix/op — body {"id":N,"email":"..."}
DeleteDELETE /api/external/fuse-metrix/op/:id

Gotchas

  • JWT expires. If /op starts returning 401/403, re-run Step 1 for a fresh token.
  • Admin role required. Login works but /op 403s → the account isn’t role_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).
Last updated on