Partners and Agents
A Partner or Agent is an integrator that acts on its own behalf or on behalf of other organisations in Zertiban. In addition to operating on its own organisation, it operates on the client organisations it represents.
Model
In a direct model, each organisation integrates Zertiban with its own M2M credentials. In a collaborator model, a single integrator authenticates with its credentials and operates on several client organisations, without each client having to manage credentials of its own.
There are two types of collaborator:
- Partner — integrates Zertiban into its own product and operates on its clients as part of the service it provides to them.
- Agent — sells and bills Zertiban services to its clients, and adds its own commission on every operation it creates for them.
To integrate, a Partner or Agent consumes two specific pieces of the API:
- Registering its client organisations in Zertiban via
POST/business/v1/businesses/{businessUuid}/business-collaborators, required before it can operate on them. See Registering a client organisation. - Delegated authentication on
POST/idp/oauth2/tokento obtain a token to act as the client for the rest of the API. See Delegated authentication.
Before a delegated token can be issued for a client, two preconditions must be met:
- The client organisation must exist and be active in Zertiban (it is registered with the endpoint above).
- An active mandate must exist between the collaborator and that client.
Without either of them, the Idp responds with 400 invalid_grant.
Registering a client organisation
A collaborator registers each of its client organisations in Zertiban with this endpoint, using its own token (from step 1 of the delegated authentication). The collaborator ↔ client link is created in the same call; from then on, once the client is active, the collaborator can request delegated tokens to operate on the client's behalf.
POST/business/v1/businesses/{businessUuid}/business-collaborators
The path {businessUuid} is the UUID of the collaborator's own organisation, and must match both the x-tenant-id header and the tenant claim in the access token.
Headers:
Authorization: Bearer {collaboratorToken}— token obtained in Step 1 — Collaborator's own login.x-tenant-id: {collaboratorBusinessUuid}— same UUID as the path.Content-Type: application/json
Request body (application/json):
| Field | Required | Description |
|---|---|---|
taxId | Yes | Tax identifier of the client organisation. |
type | Yes | COMPANY or SELF_EMPLOYER. |
legalName | Yes, if type is COMPANY | Legal name of the client organisation. Ignored for self-employed clients. |
name and lastName | Yes, if type is SELF_EMPLOYER | First name and last name of the self-employed client. Ignored for companies. |
tradeName | No | Trade name, if it differs from the legal name. |
activityCode | Yes | Business activity code, valid for the client's country. Look up valid values in Business activities. |
fiscalAddress | Yes | Fiscal address of the client organisation. Country, state, city and postal codes come from Locations. |
collaboratorType | Yes | Role you act with for this client: Partner or Agent. |
users | Yes | Exactly one initial user for the client, who will act as its administrator. |
Each entry in users has the following shape:
| Field | Required | Description |
|---|---|---|
name | Yes | First name of the initial user. |
lastName | Yes | Last name of the initial user. |
email | Yes | Email address. Receives an invitation to Zertiban. |
role | Yes | Must be ADMINISTRATOR. |
curl -i -X POST 'https://api-sandbox.zertiban.com/business/v1/businesses/{collaboratorBusinessUuid}/business-collaborators' \
-H 'Authorization: Bearer {collaboratorToken}' \
-H 'x-tenant-id: {collaboratorBusinessUuid}' \
-H 'Content-Type: application/json' \
-d '{
"taxId": "B12345678",
"type": "COMPANY",
"legalName": "Acme Client, S.L.",
"activityCode": "6201",
"fiscalAddress": {
"street": "Calle Innovación 45",
"postalCode": "28022",
"cityCode": "ES-MD-MADRID",
"stateCode": "ES-MD",
"countryCode": "ES"
},
"collaboratorType": "PARTNER",
"users": [
{
"name": "Ada",
"lastName": "Lovelace",
"email": "[email protected]",
"role": "ADMINISTRATOR"
}
]
}'response = requests.post(
f"https://api-sandbox.zertiban.com/business/v1/businesses/{COLLABORATOR_BUSINESS_UUID}/business-collaborators",
headers={
"Authorization": f"Bearer {collaborator_token}",
"x-tenant-id": COLLABORATOR_BUSINESS_UUID,
"Content-Type": "application/json",
},
json={
"taxId": "B12345678",
"type": "COMPANY",
"legalName": "Acme Client, S.L.",
"activityCode": "6201",
"fiscalAddress": {
"street": "Calle Innovación 45",
"postalCode": "28022",
"cityCode": "ES-MD-MADRID",
"stateCode": "ES-MD",
"countryCode": "ES",
},
"collaboratorType": "PARTNER",
"users": [
{
"name": "Ada",
"lastName": "Lovelace",
"email": "[email protected]",
"role": "ADMINISTRATOR",
}
],
},
)
client_business_uuid = response.json()["businessUuid"]const response = await fetch(
`https://api-sandbox.zertiban.com/business/v1/businesses/${COLLABORATOR_BUSINESS_UUID}/business-collaborators`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${collaboratorToken}`,
'x-tenant-id': COLLABORATOR_BUSINESS_UUID,
'Content-Type': 'application/json'
},
body: JSON.stringify({
taxId: 'B12345678',
type: 'COMPANY',
legalName: 'Acme Client, S.L.',
activityCode: '6201',
fiscalAddress: {
street: 'Calle Innovación 45',
postalCode: '28022',
cityCode: 'ES-MD-MADRID',
stateCode: 'ES-MD',
countryCode: 'ES'
},
collaboratorType: 'PARTNER',
users: [
{
name: 'Ada',
lastName: 'Lovelace',
email: '[email protected]',
role: 'ADMINISTRATOR'
}
]
})
}
);
const { businessUuid: clientBusinessUuid } = await response.json();Map<String, Object> body = Map.of(
"taxId", "B12345678",
"type", "COMPANY",
"legalName", "Acme Client, S.L.",
"activityCode", "6201",
"fiscalAddress", Map.of(
"street", "Calle Innovación 45",
"postalCode", "28022",
"cityCode", "ES-MD-MADRID",
"stateCode", "ES-MD",
"countryCode", "ES"
),
"collaboratorType", "PARTNER",
"users", List.of(Map.of(
"name", "Ada",
"lastName", "Lovelace",
"email", "[email protected]",
"role", "ADMINISTRATOR"
))
);
RegisterClientResponse created = WebClient.create("https://api-sandbox.zertiban.com")
.post().uri("/business/v1/businesses/{businessUuid}/business-collaborators", collaboratorBusinessUuid)
.headers(h -> {
h.setBearerAuth(collaboratorToken);
h.set("x-tenant-id", collaboratorBusinessUuid);
})
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(body)
.retrieve()
.bodyToMono(RegisterClientResponse.class)
.block();
String clientBusinessUuid = created.getBusinessUuid();Successful response (201):
{
"businessUuid": "d17b6d76-567e-46c5-8af5-1ff56a29791a"
}After registration the client organisation is not yet active: the invited administrator must accept the invitation received by email to complete the onboarding. Delegated tokens can only be issued for the client once it is active in Zertiban.
Registration errors
400— the request body is missing required fields or contains invalid values.403— the path{uuid}, thex-tenant-idheader and the tenant claim in the token do not all match.404— theactivityCodeis not valid for the client's country.409— an organisation with the sametaxIdalready exists in Zertiban, or thecollaboratorTypeyou declared is not enabled on your account.
Delegated authentication
The collaborator obtains a delegated token on POST/idp/oauth2/token in two steps. Both hit the same endpoint, both use Basic Auth (clientSecretBasic), and only the grant_type changes.
Step 1 — Collaborator's own login
The collaborator authenticates with grant_type=client_credentials and its M2M credentials, exactly like a direct client (see Authentication).
curl -i -X POST 'https://api-sandbox.zertiban.com/idp/oauth2/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u '{partnerClientId}:{partnerClientSecret}' \
--data-urlencode 'grant_type=client_credentials'import requests
response = requests.post(
"https://api-sandbox.zertiban.com/idp/oauth2/token",
auth=(PARTNER_CLIENT_ID, PARTNER_CLIENT_SECRET),
data={"grant_type": "client_credentials"}
)
subject_token = response.json()["access_token"]const credentials = Buffer.from(`${PARTNER_CLIENT_ID}:${PARTNER_CLIENT_SECRET}`).toString('base64');
const response = await fetch('https://api-sandbox.zertiban.com/idp/oauth2/token', {
method: 'POST',
headers: {
Authorization: `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'grant_type=client_credentials'
});
const { access_token: subjectToken } = await response.json();TokenResponse subject = WebClient.create("https://api-sandbox.zertiban.com")
.post().uri("/idp/oauth2/token")
.headers(h -> h.setBasicAuth(partnerClientId, partnerClientSecret))
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData("grant_type", "client_credentials"))
.retrieve()
.bodyToMono(TokenResponse.class)
.block();
String subjectToken = subject.getAccessToken();With this token the collaborator cannot yet call business endpoints on behalf of a client. It is the input for step 2.
Step 2 — Token exchange
Using the access_token from step 1 as subject_token, the collaborator calls the same endpoint again requesting a delegated token for a specific client. This is the TokenRequestDelegationTokenExchange variant of the schema documented in the OpenAPI, over the standard urn:ietf:params:oauth:grant-type:token-exchange grant from RFC 8693.
Request body parameters (application/x-www-form-urlencoded):
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | urn:ietf:params:oauth:grant-type:token-exchange. |
subject_token | Yes | The collaborator's own token obtained in step 1. |
subject_token_type | Yes | urn:ietf:params:oauth:token-type:access_token. |
target_tenant | Yes | Zertiban-specific parameter (outside RFC 8693). UUID of the target client organisation. |
requested_token_type | No | Defaults to urn:ietf:params:oauth:token-type:access_token. |
curl -i -X POST 'https://api-sandbox.zertiban.com/idp/oauth2/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u '{partnerClientId}:{partnerClientSecret}' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
--data-urlencode 'subject_token={subjectToken}' \
--data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \
--data-urlencode 'target_tenant={clientBusinessUuid}'response = requests.post(
"https://api-sandbox.zertiban.com/idp/oauth2/token",
auth=(PARTNER_CLIENT_ID, PARTNER_CLIENT_SECRET),
data={
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": subject_token,
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"target_tenant": CLIENT_BUSINESS_UUID,
},
)
delegated_token = response.json()["access_token"]const credentials = Buffer.from(`${PARTNER_CLIENT_ID}:${PARTNER_CLIENT_SECRET}`).toString('base64');
const body = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
subject_token: subjectToken,
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
target_tenant: CLIENT_BUSINESS_UUID
});
const response = await fetch('https://api-sandbox.zertiban.com/idp/oauth2/token', {
method: 'POST',
headers: {
Authorization: `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body
});
const { access_token: delegatedToken } = await response.json();TokenResponse delegated = WebClient.create("https://api-sandbox.zertiban.com")
.post().uri("/idp/oauth2/token")
.headers(h -> h.setBasicAuth(partnerClientId, partnerClientSecret))
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters
.fromFormData("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
.with("subject_token", subjectToken)
.with("subject_token_type", "urn:ietf:params:oauth:token-type:access_token")
.with("target_tenant", clientBusinessUuid))
.retrieve()
.bodyToMono(TokenResponse.class)
.block();
String delegatedToken = delegated.getAccessToken();The delegated token
Step 2 returns a standard OAuth2 TokenResponse:
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 900
}The returned access_token has the following characteristics:
| Property | Value |
|---|---|
sub / tenant_id | UUID of the client organisation (not the collaborator's) |
act claim | Nested object identifying the acting collaborator (see below) |
roles | Collaborator's roles over the client, derived from the mandate |
authorities | Authorities granted to the collaborator over the client, derived from the mandate |
client_id | Collaborator's clientId (the one that authenticated in step 1) |
expires_in | 900 s (short-lived — refresh it before it expires) |
The act claim is an object with the following shape:
act field | Value |
|---|---|
sub | Collaborator's clientId (the one that authenticated in step 1) |
tenant_id | UUID of the collaborator organisation (Partner or Agent) |
collaborator_type | PARTNER or AGENT |
Example of the decoded JWT payload:
{
"tenant_id": "d17b6d76-567e-46c5-8af5-1ff56a29791a",
"sub": "d17b6d76-567e-46c5-8af5-1ff56a29791a",
"act": {
"sub": "9b2f7c10-3a1e-4c2b-9f55-1234567890ab",
"tenant_id": "7c3e1f88-1111-2222-3333-444455556666",
"collaborator_type": "PARTNER"
},
"roles": ["…"],
"authorities": ["…"],
"client_id": "9b2f7c10-3a1e-4c2b-9f55-1234567890ab"
}Partner vs Agent
The collaborator type travels in act.collaborator_type. The contractual difference shows up when creating operations:
- Agent: when operating as an Agent, every operation must include a
commissionblock (amountin the minor unit of the currency as a positive integer, andcurrencyas an ISO 4217 code). If missing, the API responds400. See theCommissionandPagafactuCommissionschemas in the API reference. - Partner: does not send
commission; if sent it is ignored.
For direct callers (non-collaborators) the field is also ignored.
Using the delegated token
From here on, calls to business endpoints are identical to those of a direct client, with the delegated token in Authorization and the client UUID in x-tenant-id:
Authorization: Bearer {delegatedToken}
x-tenant-id: {clientBusinessUuid}All business endpoints (flows, operations, PSD2 payments, beneficiary accounts, configurations) are called exactly the same way — the token already states that you are acting as the client.
Token exchange errors
Errors returned by POST/idp/oauth2/token when step 2 fails. They are encoded as OAuth2Error ({ error, error_description }).
400 invalid_request
The target_tenant parameter is missing or its value is not a valid UUID.
400 invalid_grant
Possible causes:
- The
subject_tokenis not valid or is expired. - No active mandate exists between the collaborator and the requested
target_tenant, or the mandate has no roles assigned. Before retrying, check that the client organisation is active and that a collaborator ↔ client mandate exists.