Blockstream Enterprise Custody SDK
Onboarding

Onboarding a Regular User

How a regular user authorizes a device from an invitation and stores the permanent session credentials returned by the server.

Complete Setup and Installation before following this guide.

User types

Blockstream Enterprise Custody supports two user types:

TypeAuthentication modelTypical use
Regular userInvitation plus device authHuman operators using a dashboard or client app
API userDirect key-based onboardingProgrammatic integrations and backend services

This page covers regular-user device onboarding. For machine-oriented onboarding, see API User Onboarding Workflow.

What this flow does

In the invitation flow:

  1. an admin invites the user
  2. the user receives temporary invitation credentials
  3. the user generates permanent session keys locally
  4. the user authorizes the device with the invitation
  5. the server returns the permanent device identity

Before you begin

This guide uses:

  • @blockstream/cryptic
  • @blockstream/cryptic/utils
  • uuid

You also need:

  • an invitation payload from the server
  • the one-time ECDSA private key from that invitation flow
  • a Broadcaster for the invitation session, or one of the lower-level request helpers from Making Requests

If your deployment requires mTLS for /request, keep the onboarding sequence here and replace only the broadcaster transport with the Node-side mTLS flow from Making Requests.

What comes in the invitation

FieldMeaning
invite_stringOne-time server-issued invitation token
oneTimePrivateKeyOne-time ECDSA signing key for the authorization request
deviceUuidTemporary device identifier bound to the invitation
server_rsa_pub_key_pem_b64Base64-encoded server RSA public key
server_ecdsa_pub_keyServer ECDSA public key as hex

Step 1: Generate permanent session keys

Generate the keys that the device will keep after onboarding:

import { generateUserKeyPairs } from '@blockstream/cryptic/utils'

const sessionKeys = await generateUserKeyPairs()

Step 2: Decode the server RSA key

If the invitation contains the RSA public key as base64-encoded PEM, decode it before constructing the client:

const serverRsaPublicKeyPem = Buffer.from(invitation.server_rsa_pub_key_pem_b64, 'base64').toString(
  'utf-8',
)

Step 3: Create the one-time authorization client

The authorization request is signed with the invitation's one-time ECDSA key, but it should use the new device RSA key so the server can return encrypted data for the permanent device session.

import { Buffer } from 'buffer'
import { Blockstream } from '@blockstream/cryptic'
import { Broadcaster } from '@blockstream/cryptic/broadcaster'

const oneTimeClient = new Blockstream(
  sessionKeys.rsa.privateKeyPem,
  Buffer.from(invitation.oneTimePrivateKey, 'hex'),
  invitation.deviceUuid,
  serverRsaPublicKeyPem,
  Buffer.from(invitation.server_ecdsa_pub_key, 'hex'),
)

const oneTimeBroadcaster = new Broadcaster(ECS_BASE_URL, oneTimeClient)

Step 4: Authorize the device

import { v4 as uuidv4 } from 'uuid'

const response = await oneTimeBroadcaster.processBroadcast({
  action: 'add',
  resource: '/account-management/authorize',
  details: {
    invite_string: invitation.invite_string,
    device_name: `${uuidv4()}-device`,
    device_signature_pubkey: sessionKeys.ecdsa.publicKeyHex,
    device_encryption_pubkey: sessionKeys.rsa.publicKeyPem,
    password,
  },
})

If your environment uses passwords for user devices, pass it here. Otherwise leave it empty or follow your backend policy.

In mTLS-enabled deployments, this authorization request is still the same ECS message. Only the broadcaster transport changes, using the mTLS pattern from Making Requests.

Step 5: Store the permanent session credentials

On success, persist the final server response:

if (response.status !== 'success') {
  throw new Error(response.message ?? 'Authorization failed')
}

const session = {
  userId: response.details.user_id,
  deviceUuid: response.details.device.device_uuid,
  sessionPrivateKeys: {
    ecdsa: sessionKeys.ecdsa.privateKeyHex,
    rsa: sessionKeys.rsa.privateKeyPem,
  },
  serverPublicKeys: {
    rsa: serverRsaPublicKeyPem,
    ecdsa: invitation.server_ecdsa_pub_key,
  },
}

The returned device_uuid is now the permanent device identity to use for future authenticated requests.

Proposal state

If the authorization returns status: "pending", the request became a proposal and must be approved before the device becomes active.

if (response.status === 'pending') {
  console.log('Awaiting approval for proposal:', response.details.proposal_id)
}

On this page