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
API credentials
Client ID and secret, generated in the Notabene dashboard.
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.
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.
| Environment | Base URL and audience |
|---|---|
| Sandbox | https://sandbox.notabene.id |
| Production, EU | https://eu-a.notabene.idplaceholder |
| Production, US | https://us-a.notabene.idplaceholder |
| Environment | Base URL and audience |
|---|---|
| Sandbox | https://sandbox.notabene.id |
| Production, EU | https://eu-a.notabene.id (placeholder) |
| Production, US | https://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 credentialYOUR_CLIENT_SECRET: The client secret, shown once when you created the credential
{
"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.
Authorization: Bearer YOUR_ACCESS_TOKEN
Authorization: Bearer YOUR_ACCESS_TOKENAn 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.
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"
}
}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.
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 iturn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80: The id of the signed-in customer, as you registered them
{
"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.
| Route | What your app uses it for |
|---|---|
POST/ | Whether the Travel Rule applies to the withdrawal, and which details to collect |
POST/ | Which institution holds the destination address |
GET/, GET/ | Let your customer pick the receiving institution |
GET/, GET/ | Fill your asset and country pickers |
GET/ | Show the proofs already recorded for a self-hosted wallet |
GET/ | Show the status of your customer’s transfer |
| Route | What your app uses it for |
|---|---|
POST /api/v1/customers/{customer}/transfers/assess | Whether the Travel Rule applies to the withdrawal, and which details to collect |
POST /api/v1/customers/{customer}/wallet-ownerships/discover | Which 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}/jurisdictions | Fill your asset and country pickers |
GET /api/v1/customers/{customer}/wallets/{address}/proofs | Show 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.
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 iturn:uuid:0c8f2c3e-5b7d-4c90-a1f2-3e4d5c6b7a80: The id of the signed-in customer, as you registered them
HTTP/1.1 204 No Contentcurl 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 ContentIt 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
| Answer | What it means |
|---|---|
403 from the token URL, with Service not enabled within domain | The 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 server | The credential belongs to another environment than the audience. |
403 from the token URL, with No audience parameter was provided | The request has no audience. |
401 from the token URL, with Unauthorized | The client ID or secret is wrong, or the credential belongs to another token URL. |
401 with unauthorized | The request has no Authorization header. |
401 with invalid bearer token | The token is malformed, has expired or was revoked. Request a new one. |
403 with missing required scopes | The token does not have the permission the call needs, which the response names. |
403 with token subject does not match customer | A delegate token was used outside its customer’s routes. |
| Answer | What it means |
|---|---|
403 from the token URL, with Service not enabled within domain | The 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 server | The credential belongs to another environment than the audience. |
403 from the token URL, with No audience parameter was provided | The request has no audience. |
401 from the token URL, with Unauthorized | The client ID or secret is wrong, or the credential belongs to another token URL. |
401 with unauthorized | The request has no Authorization header. |
401 with invalid bearer token | The token is malformed, has expired or was revoked. Request a new one. |
403 with missing required scopes | The token does not have the permission the call needs, which the response names. |
403 with token subject does not match customer | A delegate token was used outside its customer’s routes. |
Updated about 6 hours ago
