Authentication

Your backend authenticates to the Notabene API by exchanging its API credential for an access token, which it sends with every call. Your own app may need to call Notabene from your customer’s device, for example to check a withdrawal as your customer types it. In that case, your backend gives the app a short-lived delegate token for that one customer instead, so your credential never leaves your backend.

Authentication with Notabene

Access tokenDelegate token

Your backend · server-to-server

1

API credentials

Client ID and secret, generated in the Notabene dashboard.

2

Access token

Requested with your credentials via OAuth 2.0. Stays on your server.

All Notabene APIs

Every call your backend makes to Notabene.

/transfers/customers/wallets/network…

Your backend uses it to request a delegate token for a customer

Your frontend · on behalf of a customer

Delegate tokens exist only for the frontend, so your access token is never exposed in a browser or app.

3

Delegate token

One per customer. Your backend passes it to your frontend.

4Helper APIs

Used by the Embedded component, or called by your own UI

/assess/entities/discover

Environments

Sandbox and production are separate, each with its own account, credentials and base URL. When you request an access token, the base URL of your environment is also the audience.

EnvironmentBase URL and audience
Sandboxhttps://sandbox.notabene.id
Production, EUhttps://eu-a.notabene.idplaceholder
Production, UShttps://us-a.notabene.idplaceholder
EnvironmentBase URL and audience
Sandboxhttps://sandbox.notabene.id
Production, EUhttps://eu-a.notabene.id (placeholder)
Production, UShttps://us-a.notabene.id (placeholder)

You request access tokens from https://auth.notabene.id/oauth/token. That production uses the same token URL is still to be confirmed. (placeholder)

Your institution lives in one region, so use the base URL your onboarding names and build against sandbox first.

Create an API credential

Once your account is ready, as described in Create your account, create an API credential in the dashboard under Settings › API Credentials (placeholder). It comes with a client ID and a client secret. Store the secret in your secrets manager straight away, because it is shown only once. A credential works only in the environment it was created in, and you can hold up to 20 (placeholder) credentials at once.

Your backend can also create a credential with POST /api/v1/api-credentials, which answers with the client_id, the client_secret and the time it was created. It needs subsidiary_access, where true also gives the credential access to all your subsidiary organizations and false limits it to yours.

Rotating a credential

To rotate a credential without downtime, create a new one, move your backend to it, and then delete the old one. Whether the access tokens of a deleted credential stop working at once, or only when they expire, is still to be confirmed. (placeholder)

Get an access token

Exchange the credential for an access token with the OAuth 2.0 client credentials grant, giving your environment’s base URL as the audience.

curl https://auth.notabene.id/oauth/token \
  --header 'Content-Type: application/json' \
  --data '{
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "grant_type": "client_credentials",
    "audience": "https://sandbox.notabene.id"
  }'const response = await fetch('https://auth.notabene.id/oauth/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "grant_type": "client_credentials",
    "audience": "https://sandbox.notabene.id"
  }),
});
const answer = response.status === 204 ? null : await response.json();import requests

response = requests.post(
    'https://auth.notabene.id/oauth/token',
    json={
      "client_id": "YOUR_CLIENT_ID",
      "client_secret": "YOUR_CLIENT_SECRET",
      "grant_type": "client_credentials",
      "audience": "https://sandbox.notabene.id"
    },
)
answer = None if response.status_code == 204 else response.json()
What to replace
  • YOUR_CLIENT_ID: The client ID of your API credential
  • YOUR_CLIENT_SECRET: The client secret, shown once when you created the credential
Response200
{
  "access_token": "eyJhbGciOiJSUzI1NiIs",
  "expires_in": 86400,
  "token_type": "Bearer"
}
curl https://auth.notabene.id/oauth/token \
  --header 'Content-Type: application/json' \
  --data '{
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "grant_type": "client_credentials",
    "audience": "https://sandbox.notabene.id"
  }'
const response = await fetch('https://auth.notabene.id/oauth/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "grant_type": "client_credentials",
    "audience": "https://sandbox.notabene.id"
  }),
});
const answer = response.status === 204 ? null : await response.json();
import requests

response = requests.post(
    'https://auth.notabene.id/oauth/token',
    json={
      "client_id": "YOUR_CLIENT_ID",
      "client_secret": "YOUR_CLIENT_SECRET",
      "grant_type": "client_credentials",
      "audience": "https://sandbox.notabene.id"
    },
)
answer = None if response.status_code == 204 else response.json()
{
  "access_token": "eyJhbGciOiJSUzI1NiIs",
  "expires_in": 86400,
  "token_type": "Bearer"
}

Keep the access_token from the response and send it on every request, in the Authorization header.

HTTP
Authorization: Bearer YOUR_ACCESS_TOKEN
Authorization: Bearer YOUR_ACCESS_TOKEN

An access token lasts 24 hours, so reuse it rather than fetch one for every call. Request a new one about 5 minutes before expires_in runs out. If you request a token again before then, you can get the same one back with less time left, so always go by expires_in.

The token is bound to your institution, so nothing in a request selects which institution you act as, and it carries the permissions of your credential. Each call’s page in the API reference names the permission it needs, and a call your token cannot make answers 403 with that permission.

To check that everything works, call GET /api/v1/identity, which answers with your DID and your DID document.

API reference
curl https://sandbox.notabene.id/api/v1/identity \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'const response = await fetch('https://sandbox.notabene.id/api/v1/identity', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer YOUR_ACCESS_TOKEN',
  },
});
const answer = response.status === 204 ? null : await response.json();import requests

response = requests.get(
    'https://sandbox.notabene.id/api/v1/identity',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
)
answer = None if response.status_code == 204 else response.json()
Response200
{
  "did": "did:web:example.com:pt",
  "document": {
    "@context": [
      "https://www.w3.org/ns/did/v1"
    ],
    "id": "did:web:example.com:pt"
  }
}
curl https://sandbox.notabene.id/api/v1/identity \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
const response = await fetch('https://sandbox.notabene.id/api/v1/identity', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer YOUR_ACCESS_TOKEN',
  },
});
const answer = response.status === 204 ? null : await response.json();
import requests

response = requests.get(
    'https://sandbox.notabene.id/api/v1/identity',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
)
answer = None if response.status_code == 204 else response.json()
{
  "did": "did:web:example.com:pt",
  "document": {
    "@context": [
      "https://www.w3.org/ns/did/v1"
    ],
    "id": "did:web:example.com:pt"
  }
}

Delegate tokens for your app

Your web or mobile app may call Notabene directly, for example to show whether the Travel Rule applies to a withdrawal and which details to ask for. Never give it your API credential or your access token: your backend requests a delegate token for the signed-in customer and hands that to the app instead. The embedded component described in Data collection works the same way.

API reference
curl https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
  }'const response = await fetch('https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_ACCESS_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
  }),
});
const answer = response.status === 204 ? null : await response.json();import requests

response = requests.post(
    'https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
    json={
      "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
    },
)
answer = None if response.status_code == 204 else response.json()
What to replace
  • did:web:example.com:pt: Your DID, as GET /api/v1/identity answers it
  • urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80: The id of the signed-in customer, as you registered them
Response201
{
  "access_token": "eyJhbGciOiJFZERTQSIs",
  "token_type": "Bearer",
  "expires_in": 3600,
  "jti": "c03044be-8e0b-4bbe-9d2c-c7b28ac3aa5f"
}
curl https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
  }'
const response = await fetch('https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_ACCESS_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
  }),
});
const answer = response.status === 204 ? null : await response.json();
import requests

response = requests.post(
    'https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
    json={
      "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
    },
)
answer = None if response.status_code == 204 else response.json()
{
  "access_token": "eyJhbGciOiJFZERTQSIs",
  "token_type": "Bearer",
  "expires_in": 3600,
  "jti": "c03044be-8e0b-4bbe-9d2c-c7b28ac3aa5f"
}

Set customer_ref to the id of a customer you registered, as described in Customers, and the DID in the path to your own. An unknown customer answers 400 with customer_ref is not a known customer of this tenant. A delegate token lasts 1 hour, so request one for each session of your customer and a new one when it expires.

What a delegate token can do

A delegate token works only on the routes under /api/v1/customers/{customer_ref}/, and only for the customer it was requested for. On another customer’s routes or anywhere else in the API, a call with it answers 403 with token subject does not match customer.

RouteWhat your app uses it for
POST/api/v1/customers/{customer}/transfers/assessWhether the Travel Rule applies to the withdrawal, and which details to collect
POST/api/v1/customers/{customer}/wallet-ownerships/discoverWhich institution holds the destination address
GET/api/v1/customers/{customer}/network/entities, GET/api/v1/customers/{customer}/network/entities/{did}Let your customer pick the receiving institution
GET/api/v1/customers/{customer}/assets, GET/api/v1/customers/{customer}/jurisdictionsFill your asset and country pickers
GET/api/v1/customers/{customer}/wallets/{address}/proofsShow the proofs already recorded for a self-hosted wallet
GET/api/v1/customers/{customer}/transfers/{id}Show the status of your customer’s transfer
RouteWhat your app uses it for
POST /api/v1/customers/{customer}/transfers/assessWhether the Travel Rule applies to the withdrawal, and which details to collect
POST /api/v1/customers/{customer}/wallet-ownerships/discoverWhich institution holds the destination address
GET /api/v1/customers/{customer}/network/entities, GET /api/v1/customers/{customer}/network/entities/{did}Let your customer pick the receiving institution
GET /api/v1/customers/{customer}/assets, GET /api/v1/customers/{customer}/jurisdictionsFill your asset and country pickers
GET /api/v1/customers/{customer}/wallets/{address}/proofsShow the proofs already recorded for a self-hosted wallet
GET /api/v1/customers/{customer}/transfers/{id}Show the status of your customer’s transfer

A delegate token cannot create transfers or record proofs. For those, your app sends what it collected to your backend, which uses its own access token.

By default, a delegate token has the permissions read:transfer, read:vasp, read:walletproof and create:address. To give it fewer, list the ones it needs in requested_scopes, but only from that default set, since any other permission answers 400.

Revoking delegate tokens

To end all of a customer’s delegate tokens at once, for example when they sign out everywhere or close their account, call POST /api/v1/entities/{your DID}/delegate-token/revoke-all with their customer_ref.

API reference
curl https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token/revoke-all \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
  }'const response = await fetch('https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token/revoke-all', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_ACCESS_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
  }),
});
const answer = response.status === 204 ? null : await response.json();import requests

response = requests.post(
    'https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token/revoke-all',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
    json={
      "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
    },
)
answer = None if response.status_code == 204 else response.json()
What to replace
  • did:web:example.com:pt: Your DID, as GET /api/v1/identity answers it
  • urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80: The id of the signed-in customer, as you registered them
Response204
HTTP/1.1 204 No Content
curl https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token/revoke-all \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
  }'
const response = await fetch('https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token/revoke-all', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_ACCESS_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
  }),
});
const answer = response.status === 204 ? null : await response.json();
import requests

response = requests.post(
    'https://sandbox.notabene.id/api/v1/entities/did:web:example.com:pt/delegate-token/revoke-all',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
    json={
      "customer_ref": "urn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80"
    },
)
answer = None if response.status_code == 204 else response.json()
HTTP/1.1 204 No Content

It answers 204 with no body, and their existing tokens stop working straight away. You can request new ones for them as before.

When authentication fails

AnswerWhat it means
403 from the token URL, with Service not enabled within domainThe audience is not a base URL the token URL knows. Check that it starts with https:// and belongs to the same environment.
403 from the token URL, with Client … is not authorized to access resource serverThe credential belongs to another environment than the audience.
403 from the token URL, with No audience parameter was providedThe request has no audience.
401 from the token URL, with UnauthorizedThe client ID or secret is wrong, or the credential belongs to another token URL.
401 with unauthorizedThe request has no Authorization header.
401 with invalid bearer tokenThe token is malformed, has expired or was revoked. Request a new one.
403 with missing required scopesThe token does not have the permission the call needs, which the response names.
403 with token subject does not match customerA delegate token was used outside its customer’s routes.
AnswerWhat it means
403 from the token URL, with Service not enabled within domainThe audience is not a base URL the token URL knows. Check that it starts with https:// and belongs to the same environment.
403 from the token URL, with Client … is not authorized to access resource serverThe credential belongs to another environment than the audience.
403 from the token URL, with No audience parameter was providedThe request has no audience.
401 from the token URL, with UnauthorizedThe client ID or secret is wrong, or the credential belongs to another token URL.
401 with unauthorizedThe request has no Authorization header.
401 with invalid bearer tokenThe token is malformed, has expired or was revoked. Request a new one.
403 with missing required scopesThe token does not have the permission the call needs, which the response names.
403 with token subject does not match customerA delegate token was used outside its customer’s routes.

Did this page help you?