Partners y Agentes
Un Partner o Agente es un integrador que actúa en su nombre o en nombre de otras organizaciones en Zertiban. Además de operar sobre su propia organización, opera sobre las organizaciones cliente que representa.
Modelo
En un modelo directo, cada organización integra Zertiban con sus propias credenciales M2M. En un modelo colaborador, un único integrador se autentica con sus credenciales y opera sobre varias organizaciones cliente, sin que cada cliente tenga que gestionar credenciales por su cuenta.
Existen dos tipos de colaborador:
- Partner — integra Zertiban dentro de su propio producto y opera sobre sus clientes como parte del servicio que les presta.
- Agente — vende y factura servicios de Zertiban a sus clientes, y añade su propia comisión en cada operación que crea para ellos.
Para integrarse, un Partner o Agente consume dos piezas específicas de la API:
- Alta de sus organizaciones cliente en Zertiban sobre
POST/business/v1/businesses/{businessUuid}/business-collaborators, previa a poder operar sobre ellas. Ver Alta de una organización cliente. - Autenticación delegada sobre
POST/idp/oauth2/tokenpara obtener un token con el que actuar como el cliente sobre el resto de la API. Ver Autenticación delegada.
Antes de poder emitir un token delegado sobre un cliente, deben cumplirse dos precondiciones:
- La organización cliente debe existir y estar activa en Zertiban (se da de alta con el endpoint anterior).
- Debe existir un mandato activo entre el colaborador y ese cliente.
Sin cualquiera de las dos, el Idp responde 400 invalid_grant.
Alta de una organización cliente
Un colaborador da de alta cada una de sus organizaciones cliente en Zertiban con este endpoint, usando su propio token (el del paso 1 de la autenticación delegada). En la misma llamada queda registrado el vínculo colaborador ↔ cliente; a partir de ahí, en cuanto el cliente esté activo, el colaborador puede solicitar tokens delegados para operar en su nombre.
POST/business/v1/businesses/{businessUuid}/business-collaborators
En el path, {businessUuid} es el UUID de la propia organización del colaborador, y debe coincidir con la cabecera x-tenant-id y con el claim de tenant del token.
Cabeceras:
Authorization: Bearer {collaboratorToken}— token obtenido en Paso 1 — Login propio del colaborador.x-tenant-id: {collaboratorBusinessUuid}— mismo UUID que el path.Content-Type: application/json
Cuerpo (application/json):
| Campo | Obligatorio | Descripción |
|---|---|---|
taxId | Sí | Identificación fiscal de la organización cliente. |
type | Sí | COMPANY o SELF_EMPLOYER. |
legalName | Sí, si type es COMPANY | Razón social de la organización cliente. Ignorado para autónomos. |
name y lastName | Sí, si type es SELF_EMPLOYER | Nombre y apellidos del autónomo. Ignorados para empresas. |
tradeName | No | Nombre comercial, si difiere de la razón social. |
activityCode | Sí | Código de actividad de la organización, válido para el país del cliente. Consulta los valores válidos en Actividades empresariales. |
fiscalAddress | Sí | Domicilio fiscal de la organización cliente. Los códigos de país, estado, ciudad y postal se obtienen de Localizaciones. |
collaboratorType | Sí | Rol con el que actúas sobre este cliente: Partner o Agente. |
users | Sí | Exactamente un usuario inicial del cliente, que actuará como su administrador. |
Cada entrada de users tiene esta forma:
| Campo | Obligatorio | Descripción |
|---|---|---|
name | Sí | Nombre del usuario inicial. |
lastName | Sí | Apellidos del usuario inicial. |
email | Sí | Correo electrónico. Recibe una invitación a Zertiban. |
role | Sí | Debe ser 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 Cliente, 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 Cliente, 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 Cliente, 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 Cliente, 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();Respuesta correcta (201):
{
"businessUuid": "d17b6d76-567e-46c5-8af5-1ff56a29791a"
}Tras el alta, la organización cliente aún no está activa: el administrador invitado debe aceptar la invitación que recibe por email para completar el onboarding. Solo se pueden emitir tokens delegados sobre el cliente cuando esté activo en Zertiban.
Errores del alta
400— falta algún campo obligatorio del cuerpo o contiene valores inválidos.403— el{uuid}del path, la cabecerax-tenant-idy el claim de tenant del token no coinciden entre sí.404— elactivityCodeno es válido para el país del cliente.409— ya existe una organización con el mismotaxIden Zertiban, o elcollaboratorTypedeclarado no está habilitado en tu cuenta.
Autenticación delegada
El colaborador obtiene un token delegado sobre POST/idp/oauth2/token en dos pasos. Ambos van al mismo endpoint, ambos con Basic Auth (clientSecretBasic), y solo cambia el grant_type.
Paso 1 — Login propio del colaborador
El colaborador se autentica con grant_type=client_credentials y sus credenciales M2M, exactamente igual que un cliente directo (ver Autenticación).
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();Con este token el colaborador todavía no puede llamar a los endpoints de negocio en nombre de un cliente. Sirve como entrada del paso 2.
Paso 2 — Token exchange
Con el access_token del paso 1 como subject_token, el colaborador vuelve al mismo endpoint pidiendo un token delegado para un cliente concreto. Es la variante TokenRequestDelegationTokenExchange del schema documentado en el OpenAPI, sobre el grant estándar urn:ietf:params:oauth:grant-type:token-exchange de RFC 8693.
Parámetros del cuerpo (application/x-www-form-urlencoded):
| Parámetro | Obligatorio | Descripción |
|---|---|---|
grant_type | Sí | urn:ietf:params:oauth:grant-type:token-exchange. |
subject_token | Sí | Token propio del colaborador obtenido en el paso 1. |
subject_token_type | Sí | urn:ietf:params:oauth:token-type:access_token. |
target_tenant | Sí | Parámetro específico de Zertiban (fuera de RFC 8693). UUID de la organización cliente destino. |
requested_token_type | No | Por defecto 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();El token delegado
El paso 2 devuelve un TokenResponse estándar de OAuth2:
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 900
}El access_token devuelto tiene estas particularidades:
| Característica | Valor |
|---|---|
sub / tenant_id | UUID de la organización cliente (no del colaborador) |
Claim act | Estructura anidada que identifica al colaborador que actúa (ver más abajo) |
roles | Roles del colaborador sobre el cliente, derivados del mandato |
authorities | Autoridades otorgadas al colaborador sobre el cliente, derivadas del mandato |
client_id | clientId del colaborador (el que se autenticó en el paso 1) |
expires_in | 900 s (vida corta — renuévalo antes de que caduque) |
El claim act es un objeto con la siguiente forma:
Campo del act | Valor |
|---|---|
sub | clientId del colaborador (el que se autenticó en el paso 1) |
tenant_id | UUID de la organización colaboradora (Partner o Agente) |
collaborator_type | PARTNER o AGENT |
Ejemplo del payload JWT decodificado:
{
"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"
}Diferencia entre Partner y Agente
El tipo de colaborador viaja en act.collaborator_type. La diferencia contractual aparece al crear operaciones:
- Agente: al operar como Agente, cada operación debe incluir un bloque
commission(amounten la unidad menor de la divisa como entero positivo, ycurrencycomo código ISO 4217). Si falta, la API responde400. Ver los schemasCommissionyPagafactuCommissionen la referencia de la API. - Partner: no envía
commission; si se envía se ignora.
En los clientes directos (no colaboradores) el campo también se ignora.
Usar el token delegado
Desde aquí, las llamadas a los endpoints de negocio son idénticas a las de un cliente directo, con el token delegado en Authorization y el UUID del cliente en x-tenant-id:
Authorization: Bearer {delegatedToken}
x-tenant-id: {clientBusinessUuid}Todos los endpoints de negocio (flujos, operaciones, pagos PSD2, cuentas beneficiarias, configuraciones) se llaman exactamente igual — el token ya dice que actúas como el cliente.
Errores del token exchange
Errores devueltos por POST/idp/oauth2/token cuando el paso 2 falla. Se codifican como OAuth2Error ({ error, error_description }).
400 invalid_request
Falta el parámetro target_tenant o el valor no es un UUID válido.
400 invalid_grant
Causas posibles:
- El
subject_tokenno es válido o está expirado. - No existe un mandato activo entre el colaborador y el
target_tenantsolicitado, o el mandato no tiene roles asignados. Antes de reintentar, comprueba que la organización cliente esté activa y que el mandato colaborador ↔ cliente exista.