Blockstream Enterprise Custody SDK
Issuer Guide (SDK)

Authentication

AMP2 authentication uses a two-phase process: one-time device authorization with invite data, followed by durable broadcaster creation for subsequent API calls.

AMP2 authentication is a two phase process: a one time device authorization using invite data, followed by durable broadcaster creation for all subsequent API calls.

Invite data payload

When Blockstream onboards you as an issuer, you receive invite data containing:

FieldTypePurpose
device_uuidUUID stringUnique identifier for this device/integration
invite_stringstringOne time activation code (consumed on first use)
One time private keyhex stringSigns the initial authorization request
Server RSA public keyPEM stringEncrypts your request payloads to AMP2
Server ECDSA public keycompressed hexVerifies AMP2's response signatures
API base URLURL stringThe AMP2 server for this environment

The invite is typically delivered through a secure URL. Each invite is single use.

Phase 1: Device authorization

Generate device key pairs

Every AMP2 device needs its own RSA and ECDSA key pairs. The @blockstream/cryptic package provides a helper:

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

const deviceKeys = await generateUserKeyPairs();

This produces:

  • deviceKeys.rsa.privateKeyPem: Promise<string> resolving to the RSA-2048 private key (PKCS#8 PEM). Used to decrypt AMP2 responses.
  • deviceKeys.rsa.publicKeyPem: Promise<string> resolving to the RSA-2048 public key (SPKI PEM). Registered with AMP2 for response encryption.
  • deviceKeys.ecdsa.privateKeyHex: ECDSA P-256 private key as a hex string. Pass Buffer.from(deviceKeys.ecdsa.privateKeyHex, 'hex') to Blockstream.
  • deviceKeys.ecdsa.publicKeyHex: ECDSA P-256 compressed public key (hex). Registered with AMP2 for signature verification.

Store the private keys securely. They are the cryptographic identity of this device and should never be exposed outside your server environment.

Create the one time broadcaster

The one time broadcaster uses the invite's throwaway private key for the initial authorization:

import { Buffer } from 'node:buffer';
import { Blockstream } from '@blockstream/cryptic';

const rsaPrivateKeyPem = await deviceKeys.rsa.privateKeyPem;

const oneTimeBroadcaster = new Blockstream(
  rsaPrivateKeyPem,
  Buffer.from(oneTimePrivateKey, 'hex'),
  inviteInfo.device_uuid!,
  SERVER_RSA_PUBLIC_KEY_PEM,
  Buffer.from(SERVER_ECDSA_PUBLIC_KEY, 'hex'),
);

The one time private key is used only for ECDSA signing of this single request. After authorization succeeds, it is consumed and cannot be reused.

Set up the broadcastRequest helper

See the Making Requests guide for a full broadcaster implementation. A minimal version of example to show:

async function broadcastRequest<T>(
  payload: object,
  blockstream: Blockstream,
): Promise<T> {
  const encrypted = await blockstream.request(payload);
  const res = await fetch(`${ECS_BASE_URL}/request`, {
    method: 'POST',
    body: encrypted,
    headers: { 'Content-Type': 'application/octet-stream' },
  });
  const bytes = await res.arrayBuffer();
  const result = await blockstream.parse<T>(new Uint8Array(bytes));
  return result.payload as T;
}

Authorize the device

The authorize endpoint is an ECS endpoint. Use the AuthActions action map from @blockstream/ecs-registry:

import { AuthActions } from '@blockstream/ecs-registry';

const authorizeResponse = await broadcastRequest<
  typeof AuthActions.authorize
>(
  {
    action: 'add',
    resource: '/account-management/authorize',
    details: {
      invite_string: inviteInfo.invite_string!,
      device_name: 'Production Issuer Backend',
      password: 'StrongPassword123!',
      device_signature_pubkey: deviceKeys.ecdsa.publicKeyHex,
      device_encryption_pubkey: await deviceKeys.rsa.publicKeyPem,
    },
  },
  oneTimeBroadcaster,
);

What this call does:

  • Consumes the invite_string: it cannot be used again.
  • Sets the account password for future token based authentication.
  • Registers device_signature_pubkey: AMP2 will use this ECDSA key to verify all future request signatures from this device.
  • Registers device_encryption_pubkey: AMP2 will encrypt all responses to this RSA key.

Error handling

if (authorizeResponse.status !== 'success') {
  throw new Error(
    `Authorization failed: ec=${authorizeResponse.error_code} message=${authorizeResponse.message}`
  );
}

Common failures:

  • Invite already used: each invite string is single use. Request a new one.
  • Server key mismatch: ensure the server public keys match the target environment (testnet vs. mainnet).
  • Malformed keys: verify RSA key is valid PEM, device ECDSA public key is 66 character compressed hex, and server ECDSA public key is decoded with Buffer.from(hex, 'hex') (not new Uint8Array(string)).

Phase 2: Durable broadcaster

After authorization, create a broadcaster that uses your permanent device keys:

import { Buffer } from 'node:buffer';
import { Blockstream } from '@blockstream/cryptic';

const broadcaster = new Blockstream(
  await deviceKeys.rsa.privateKeyPem,
  Buffer.from(deviceKeys.ecdsa.privateKeyHex, 'hex'),
  inviteInfo.device_uuid!,
  SERVER_RSA_PUBLIC_KEY_PEM,
  Buffer.from(SERVER_ECDSA_PUBLIC_KEY, 'hex'),
);

This broadcaster handles all subsequent SDK requests. Every call will be:

  1. CBOR encoded
  2. ECDSA signed with your device key
  3. AES encrypted with an ephemeral key
  4. RSA wrapped for the server

The process is transparent: the broadcastRequest function handles all COSE operations automatically.

Token management

After authorization, you need an API token for authenticated requests. The token is a bearer token included in the COSE encrypted request headers.

Get token

Retrieve your current API token using your account credentials:

The auth endpoints (/auth/token, /auth/refresh-token, /auth/change-password) are not modeled by a dedicated entry in AmpActions; the SDK treats them as generic broadcast calls and you supply the response shape inline.

type TokenResponse = {
  status: 'success' | 'failed';
  message?: string;
  details: { token: string };
};

const tokenResponse = await broadcastRequest<TokenResponse>({
  action: 'add',
  resource: '/auth/token',
  details: {
    username: 'your_username',
    password: 'your_password',
  },
}, broadcaster);

const token = tokenResponse.details.token;

This returns the existing API token for the account. Use it in subsequent authenticated requests.

Refresh token

Generate a new API token, invalidating the previous one:

const refreshResponse = await broadcastRequest<TokenResponse>({
  action: 'add',
  resource: '/auth/refresh-token',
  details: {
    username: 'your_username',
    password: 'your_password',
  },
}, broadcaster);

const newToken = refreshResponse.details.token;

Unlike get-token, this generates a fresh token and stores it server side. Use this when you need to rotate the active token.

Password management

Change password

Update the account password. Requires the current credentials:

const changePasswordResponse = await broadcastRequest<TokenResponse>({
  action: 'edit',
  resource: '/auth/change-password',
  details: {
    username: 'your_username',
    password: 'current_password',
    'new-password': 'new_strong_password',
  },
}, broadcaster);

The response returns the existing API token. The token is not rotated by a password change.

Session management

The broadcaster maintains COSE encryption state internally. For long running services:

  • Token lifecycle: tokens do not expire automatically, but you can rotate them at any time using the refresh-token endpoint. If you suspect a token has been compromised, call refresh-token immediately.
  • Key persistence: store your device keys securely and load them at service startup. You do not need to reauthorize unless you are setting up a new device.
  • Multiple devices: each device needs its own key pairs and invite. A single issuer account can have multiple authorized devices.

Security notes

  • Server side only: never expose the broadcaster or private keys in client facing code (browsers, mobile apps, public endpoints).
  • Key storage: use a secret manager (AWS Secrets Manager, HashiCorp Vault, etc.) for private keys in production.
  • Environment isolation: use separate key pairs and invite data for testnet and mainnet.
  • Password strength: the account password is used for token based auth. Use a strong, unique password.

Next steps

On this page