Corporate Management

Overview

Corporate management lets businesses onboard as entities on the Bakkt platform. This guide covers creating a corporate, linking users to it with roles, updating corporate details, and unlinking users.

KYB (Know Your Business) verification — which the corporate must clear before it can transact — is covered in KYC & KYB Verification.

Before you begin

  • Sandbox base URL: https://sandbox.api.bakkt.com
  • Production base URL: https://api.bakkt.com
  • Headers on every call: Authorization: <YOUR_API_KEY>. Some endpoints additionally require bakkt-session-id (a user session) — see each endpoint below. See Authentication for how to obtain a session.

Corporate status lifecycle

A corporate progresses through KYB verification before it can transact. The happy path:

CREATED → KYB_NEEDED → PENDING_KYB_DATA → KYB_PENDING → ACTIVE

KYB_PENDING can also resolve to a failure state:

  • SOFT_KYB_FAILED — recoverable; the admin can resubmit, looping back to KYB_PENDING.
  • HARD_KYB_FAILED — terminal; contact support.

A corporate can also be moved to SUSPENDED from any state by a manual compliance action.

StatusMeaningCan transact?Next action
CREATEDCorporate entity has just been created.NoGenerate the KYB form URL.
KYB_NEEDEDCorporate exists but no KYB form has been generated yet.NoGET /corporate/{corporate_uuid}/kyb/applicant/url
PENDING_KYB_DATAKYB form generated; corporate hasn't submitted yet.NoSend the form URL to the corporate admin.
KYB_PENDINGKYB submitted; under compliance review.NoWait for webhook or poll GET /corporate/{corporate_uuid}.
SOFT_KYB_FAILEDRecoverable failure (e.g. missing document).NoRe-issue the form URL and have the admin resubmit.
HARD_KYB_FAILEDTerminal failure.NoContact support.
ACTIVEKYB approved.Yes
SUSPENDEDSuspended manually by Bakkt compliance.NoContact support.

Note on sandbox: the sandbox PATCH /corporate/{corporate_uuid}/kyb/verification endpoint also accepts a legacy REJECTED value. Treat it as a sandbox shortcut for HARD_KYB_FAILED — the production GET response uses the soft/hard failure statuses above.

Create a corporate

Create a new corporate entity. This call does not require a session — it's typically made server-side using your API key.

Endpoint: POST /corporate — returns 201 Created with { corporate_uuid, status }.

Required fields

  • legal_name — official registered business name
  • type — business structure (see business types below)
  • contact_details — primary contact (name, email, phone)
  • registered_address — legal registered address
  • target_address — Ethereum address used for digital assets operations (must be a valid 40-hex-character 0x address)

Request example

const res = await fetch('https://sandbox.api.bakkt.com/corporate', {
  method: 'POST',
  headers: {
    'Authorization': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    legal_name: 'Acme Corporation Ltd',
    type: 'LIMITED_LIABILITY',
    registration_number: '12345678',
    contact_details: {
      name: 'John Doe',
      email: '[email protected]',
      phone: '+447871556837'
    },
    registered_address: {
      address_line_1: '123 Business Street',
      post_code: 'SW1A 1AA',
      city: 'London',
      country: 'GB'
    },
    target_address: '0xFaFe15f71861609464a4ACada29a92c5bC01637a',
    // The three accepted_* fields below are only required if hash validation is
    // enabled for your merchant. Omit them otherwise. See /docs/terms-and-conditions-hashes.
    accepted_terms_and_condition_hash: '<base64-hash>',
    accepted_privacy_policy_hash: '<base64-hash>',
    accepted_terms_of_service_hash: '<base64-hash>'
  })
});

const { corporate_uuid, status } = await res.json();
console.log('Corporate UUID:', corporate_uuid);
console.log('Status:', status); // 'CREATED'

Three legal-acceptance hashes are required. Each hash is the base64 of the document version the user agreed to. The current canonical URLs for the documents are listed in the API reference.

Business types

ValueDescription
LIMITED_LIABILITYLimited Liability Company
SOLE_TRADERSole Proprietorship
PARTNERSHIPPartnership
PUBLIC_LIMITED_COMPANYPublic Limited Company
JOINT_STOCK_COMPANYJoint Stock Company
CHARITYCharitable Organisation

Country-specific fields

Some jurisdictions require additional fields:

CountryFieldNotes
Nigeria (NG)bvnBank Verification Number — required for corporates registered in Nigeria.

Optional fields

  • registration_number — official corporate registration number.
  • trading_address — day-to-day operations address, if different from registered_address.
  • target_solana_address — Solana account address (must be active and rent-exempt). See Digital assets addresses below.

Digital assets addresses

Both the Ethereum (target_address) and Solana (target_solana_address) target addresses can be classified for compliance:

  • target_address_type / target_solana_address_type: either HOSTED (custodial — held by a VASP on the user's behalf) or SELF_HOSTED (the corporate controls the keys).
  • target_address_vasp / target_solana_address_vasp: when the type is HOSTED, optionally identify the VASP (e.g. 'Coinbase').

Setting these helps Bakkt apply the right Travel Rule treatment. See the API reference for the full schema.

Update a corporate

Update corporate details after creation. Requires a session (bakkt-session-id) for an admin user linked to the corporate.

Endpoint: PATCH /corporate/{corporate_uuid} — returns 204 No Content.

await fetch(`https://sandbox.api.bakkt.com/corporate/${corporateUuid}`, {
  method: 'PATCH',
  headers: {
    'Authorization': 'YOUR_API_KEY',
    'bakkt-session-id': sessionId,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    trading_address: {
      address_line_1: '456 Trading Street',
      post_code: 'SW1A 2BB',
      city: 'London',
      country: 'GB'
    },
    target_solana_address: '3KWxkxHLnxTqg1PMXUljpeFffzYxS7dziqWvJgQNg8dq'
  })
});

You can patch any of the fields accepted by POST /corporate (legal name, type, contact details, addresses, target addresses, country-specific fields, hashes). Omit fields you don't want to change.

Get corporate details

Retrieve the corporate profile and current KYB status.

Endpoint: GET /corporate/{corporate_uuid} — returns the full corporate object.

This endpoint accepts either bakkt-session-id (admin user's session) or user-uuid (server-to-server, where the user is linked to the corporate) as the auth header.

const res = await fetch(
  `https://sandbox.api.bakkt.com/corporate/${corporateUuid}`,
  {
    headers: {
      'Authorization': 'YOUR_API_KEY',
      'bakkt-session-id': sessionId
      // or: 'user-uuid': userUuid
    }
  }
);
const corporate = await res.json();

Example response:

{
  "legal_name": "Acme Corporation Ltd",
  "type": "LIMITED_LIABILITY",
  "registration_number": "12345678",
  "contact_details": {
    "name": "John Doe",
    "email": "[email protected]",
    "phone": "+447871556837"
  },
  "registered_address": { "...": "..." },
  "trading_address": { "...": "..." },
  "target_address": "0xFaFe15f71861609464a4ACada29a92c5bC01637a",
  "target_solana_address": "3KWxkxHLnxTqg1PMXUljpeFffzYxS7dziqWvJgQNg8dq",
  "status": "ACTIVE"
}

The response may also include a customFeeConfig object if Bakkt has configured custom fees for your corporate. This is set up out-of-band by Bakkt support.

Add a user to a corporate

Link an individual user to a corporate entity with a specific role. This call does not require a session — it's typically made server-side once you've created both the user and the corporate.

Endpoint: POST /corporate/{corporate_uuid}/user — returns 204 No Content.

await fetch(
  `https://sandbox.api.bakkt.com/corporate/${corporateUuid}/user`,
  {
    method: 'POST',
    headers: {
      'Authorization': 'YOUR_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      user_uuid: '5594401c-0072-4df2-be9c-d491c0754c21',
      role: 'ADMINISTRATOR'
    })
  }
);

Roles

RoleDescription
ADMINISTRATORFull access to corporate operations.

ADMINISTRATOR is currently the only supported role. Check the API reference for the authoritative list.

Unlink a user from a corporate

Remove a user's link to a corporate. This requires a session for a user with the ADMINISTRATOR role on that corporate.

Endpoint: DELETE /corporate/{corporate_uuid}/user/{user_uuid} — returns 204 No Content.

await fetch(
  `https://sandbox.api.bakkt.com/corporate/${corporateUuid}/user/${userUuid}`,
  {
    method: 'DELETE',
    headers: {
      'Authorization': 'YOUR_API_KEY',
      'bakkt-session-id': adminSessionId
    }
  }
);

Admin only. Calls from a non-admin session for the corporate are rejected with 401 Unauthorized.

Complete corporate onboarding workflow

const BASE_URL = 'https://sandbox.api.bakkt.com';
const headers = {
  'Authorization': API_KEY,
  'Content-Type': 'application/json'
};

const corpRes = await fetch(`${BASE_URL}/corporate`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    legal_name: 'Acme Corporation Ltd',
    type: 'LIMITED_LIABILITY',
    contact_details: {
      name: 'John Doe',
      email: '[email protected]',
      phone: '+447871556837'
    },
    registered_address: {
      address_line_1: '123 Business Street',
      post_code: 'SW1A 1AA',
      city: 'London',
      country: 'GB'
    },
    target_address: '0xFaFe15f71861609464a4ACada29a92c5bC01637a',
    // The three accepted_* fields below are only required if hash validation is
    // enabled for your merchant. Omit them otherwise. See /docs/terms-and-conditions-hashes.
    accepted_terms_and_condition_hash: '<base64-hash>',
    accepted_privacy_policy_hash: '<base64-hash>',
    accepted_terms_of_service_hash: '<base64-hash>'
  })
});
const { corporate_uuid } = await corpRes.json();

const adminRes = await fetch(`${BASE_URL}/user`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    first_name: 'John',
    last_name: 'Doe',
    email: '[email protected]',
    country: 'GB',
    target_address: '0xFaFe15f71861609464a4ACada29a92c5bC01637a',
    // The three accepted_* fields below are only required if hash validation is
    // enabled for your merchant. Omit them otherwise. See /docs/terms-and-conditions-hashes.
    accepted_terms_and_conditions_hash: '<base64-hash>',
    accepted_privacy_policy_hash: '<base64-hash>',
    accepted_terms_of_service_hash: '<base64-hash>'
  })
});
const { user_uuid } = await adminRes.json();

await fetch(`${BASE_URL}/corporate/${corporate_uuid}/user`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ user_uuid, role: 'ADMINISTRATOR' })
});

const urlRes = await fetch(
  `${BASE_URL}/corporate/${corporate_uuid}/kyb/applicant/url`,
  {
    headers: {
      'Authorization': API_KEY,
      'user-uuid': user_uuid
    }
  }
);
const { url: kybUrl } = await urlRes.json();
console.log('Send this URL to the admin:', kybUrl);

// The admin completes the Sumsub-hosted form externally.
// Bakkt fires a webhook when the corporate transitions to ACTIVE
// (or to SOFT_KYB_FAILED / HARD_KYB_FAILED). See /docs/webhooks.

In production, prefer Webhooks over polling — they fire as soon as the corporate status changes.

Multi-corporate users

A single user can be linked to multiple corporates. The user object exposes linked_corporates_uuid:

const res = await fetch('https://sandbox.api.bakkt.com/user', {
  headers: {
    'Authorization': 'YOUR_API_KEY',
    'bakkt-session-id': sessionId
  }
});
const user = await res.json();

console.log('Linked corporates:', user.linked_corporates_uuid);
// [ "corp-1-uuid", "corp-2-uuid", "corp-3-uuid" ]

To act on a specific corporate, pass its corporate_uuid in the URL of the relevant endpoint — there's no "active corporate" concept on the user session itself.

Best practices

Setup

  • Create the admin user before creating the corporate so you can link them immediately. The corporate cannot start KYB without at least one admin.
  • Communicate KYB requirements to the admin upfront — the Sumsub form will ask for company documents, beneficial-ownership info, and business activity details.
  • Set both registered_address and trading_address if they differ. KYB verification uses both.
  • If hash validation is enabled for your merchant (it's opt-in), submit the three legal-acceptance hashes (accepted_terms_and_condition_hash, accepted_privacy_policy_hash, accepted_terms_of_service_hash) on creation. The corporate flow hashes the same three documents as the user flow — only the field name for the T&C hash differs (singular ..._condition_hash for corporates, plural ..._conditions_hash for users). See Terms and Conditions Hashes for the source documents, the procedure, and how to confirm whether the feature is enabled for your merchant. If hash validation isn't enabled, omit these fields.

User and role management

  • Today only ADMINISTRATOR exists, so be deliberate about who you link — every linked user has full access.
  • Audit linked_corporates_uuid on user objects periodically to catch stale links.
  • Use DELETE /corporate/{corporate_uuid}/user/{user_uuid} from an admin session to revoke access.

Address management

  • Registered address: legal address on the company's incorporation documents.
  • Trading address: day-to-day operations address (optional; can be the same as registered).
  • Keep both addresses in sync with what the corporate submits in the KYB form to avoid WRONG_ADDRESS rejections — see KYC & KYB → Rejection reasons.

Digital assets addresses

  • Use target_address_type / target_solana_address_type (HOSTED or SELF_HOSTED) and target_address_vasp / target_solana_address_vasp to classify the destination correctly. This is required for Travel Rule compliance on outbound transfers.

Next steps