Skip to content
Developer Docs

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:

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):

FieldRequiredDescription
taxIdYesTax identifier of the client organisation.
typeYesCOMPANY or SELF_EMPLOYER.
legalNameYes, if type is COMPANYLegal name of the client organisation. Ignored for self-employed clients.
name and lastNameYes, if type is SELF_EMPLOYERFirst name and last name of the self-employed client. Ignored for companies.
tradeNameNoTrade name, if it differs from the legal name.
activityCodeYesBusiness activity code, valid for the client's country. Look up valid values in Business activities.
fiscalAddressYesFiscal address of the client organisation. Country, state, city and postal codes come from Locations.
collaboratorTypeYesRole you act with for this client: Partner or Agent.
usersYesExactly one initial user for the client, who will act as its administrator.

Each entry in users has the following shape:

FieldRequiredDescription
nameYesFirst name of the initial user.
lastNameYesLast name of the initial user.
emailYesEmail address. Receives an invitation to Zertiban.
roleYesMust be ADMINISTRATOR.
shell
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"
      }
    ]
  }'
python
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"]
javascript
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();
java
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):

json
{
  "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}, the x-tenant-id header and the tenant claim in the token do not all match.
  • 404 — the activityCode is not valid for the client's country.
  • 409 — an organisation with the same taxId already exists in Zertiban, or the collaboratorType you 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).

shell
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'
python
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"]
javascript
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();
java
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):

ParameterRequiredDescription
grant_typeYesurn:ietf:params:oauth:grant-type:token-exchange.
subject_tokenYesThe collaborator's own token obtained in step 1.
subject_token_typeYesurn:ietf:params:oauth:token-type:access_token.
target_tenantYesZertiban-specific parameter (outside RFC 8693). UUID of the target client organisation.
requested_token_typeNoDefaults to urn:ietf:params:oauth:token-type:access_token.
shell
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}'
python
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"]
javascript
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();
java
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:

json
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 900
}

The returned access_token has the following characteristics:

PropertyValue
sub / tenant_idUUID of the client organisation (not the collaborator's)
act claimNested object identifying the acting collaborator (see below)
rolesCollaborator's roles over the client, derived from the mandate
authoritiesAuthorities granted to the collaborator over the client, derived from the mandate
client_idCollaborator's clientId (the one that authenticated in step 1)
expires_in900 s (short-lived — refresh it before it expires)

The act claim is an object with the following shape:

act fieldValue
subCollaborator's clientId (the one that authenticated in step 1)
tenant_idUUID of the collaborator organisation (Partner or Agent)
collaborator_typePARTNER or AGENT

Example of the decoded JWT payload:

json
{
  "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 commission block (amount in the minor unit of the currency as a positive integer, and currency as an ISO 4217 code). If missing, the API responds 400. See the Commission and PagafactuCommission schemas 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:

http
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_token is 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.