Developer Docs
Español
Español
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.
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:
Para integrarse, un Partner o Agente consume dos piezas específicas de la API:
POST/business/v1/businesses/{businessUuid}/business-collaborators, previa a poder operar sobre ellas. Ver Alta de una organización cliente.POST/idp/oauth2/token para 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:
BUSINESS_ACTIVATED; ver Cuándo se activa el cliente.Sin cualquiera de las dos, el Idp responde 400 invalid_grant.
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/jsonCuerpo (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 | No | Código de actividad de la organización, válido para el país del cliente. Consulta los valores válidos en Actividades empresariales. Si lo envías, el cliente lo verá precargado para confirmarlo; si no, se le pedirá en el Dashboard (ver Enviar o no activityCode). |
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. |
products | Sí | Al menos un código de los productos que tienes contratados para comercializar con el collaboratorType declarado: pagafactu o zertipay. Un código repetido se trata una sola vez. |
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. |
activityCode No enviar activityCode alarga el onboarding de tu cliente
Si no envías activityCode, el alta se acepta, pero tu cliente tendrá un paso más en su onboarding: se le pedirá su actividad en el Dashboard cuando acceda. Envíalo siempre que lo conozcas para que el alta de tu cliente sea más corta.
Si lo envías, se valida contra el catálogo de actividades del país del domicilio fiscal y el cliente lo verá precargado para confirmarlo al entrar.
Alta con activityCode:
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": "20",
"fiscalAddress": {
"street": "Calle Innovación 45",
"postalCode": "28022",
"cityCode": "ES-MD-MADRID",
"stateCode": "ES-MD",
"countryCode": "ES"
},
"collaboratorType": "PARTNER",
"products": ["pagafactu", "zertipay"],
"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": "20",
"fiscalAddress": {
"street": "Calle Innovación 45",
"postalCode": "28022",
"cityCode": "ES-MD-MADRID",
"stateCode": "ES-MD",
"countryCode": "ES",
},
"collaboratorType": "PARTNER",
"products": ["pagafactu", "zertipay"],
"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: '20',
fiscalAddress: {
street: 'Calle Innovación 45',
postalCode: '28022',
cityCode: 'ES-MD-MADRID',
stateCode: 'ES-MD',
countryCode: 'ES'
},
collaboratorType: 'PARTNER',
products: ['pagafactu', 'zertipay'],
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", "20",
"fiscalAddress", Map.of(
"street", "Calle Innovación 45",
"postalCode", "28022",
"cityCode", "ES-MD-MADRID",
"stateCode", "ES-MD",
"countryCode", "ES"
),
"collaboratorType", "PARTNER",
"products", List.of("pagafactu", "zertipay"),
"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();Alta sin activityCode (tu cliente tendrá que indicar su actividad en el Dashboard):
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.",
"fiscalAddress": {
"street": "Calle Innovación 45",
"postalCode": "28022",
"cityCode": "ES-MD-MADRID",
"stateCode": "ES-MD",
"countryCode": "ES"
},
"collaboratorType": "PARTNER",
"products": ["pagafactu", "zertipay"],
"users": [
{
"name": "Ada",
"lastName": "Lovelace",
"email": "[email protected]",
"role": "ADMINISTRATOR"
}
]
}'Respuesta correcta (201):
{
"businessUuid": "d17b6d76-567e-46c5-8af5-1ff56a29791a",
"products": ["pagafactu", "zertipay"]
}products contiene los productos dados de alta para el cliente, en orden alfabético.
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.
No necesitas hacer polling para averiguarlo. Cuando la organización cliente completa su onboarding y queda activa, Zertiban emite el webhook BUSINESS_ACTIVATED, con el UUID de esa organización en resource.uuid y tu UUID de colaborador en metadata.collaboration.businessUuid. Al recibirlo ya puedes pedir un token delegado sobre ese cliente, usando resource.uuid como target_tenant.
Como el resto de webhooks, su recepción debe habilitarse previamente con Zertiban. El detalle del evento está en Eventos de organización.
400 — falta algún campo obligatorio del cuerpo o contiene valores inválidos. En particular, cuando products no se envía o viene vacío, o algún código de products no existe en el catálogo de productos de Zertiban.403 — el {uuid} del path, la cabecera x-tenant-id y el claim de tenant del token no coinciden entre sí.404 BUSINESS-SERVICE-BUSINESS-ACTIVITY-NOT-FOUND — el activityCode enviado no existe para el país del cliente. Solo aplica cuando envías activityCode.409 BUSINESS-SERVICE-COLLABORATOR-PRODUCT-NOT-OFFERED — algún producto de products no lo comercializas con el collaboratorType declarado.409 — ya existe una organización con el mismo taxId en Zertiban, o el collaboratorType declarado no está habilitado en tu cuenta.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.
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.
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 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"
}El tipo de colaborador viaja en act.collaborator_type. La diferencia contractual aparece al crear operaciones:
commission (amount en la unidad menor de la divisa como entero positivo, y currency como código ISO 4217). Si falta, la API responde 400. Ver los schemas Commission y PagafactuCommission en la referencia de la API.commission; si se envía se ignora.En los clientes directos (no colaboradores) el campo también se ignora.
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 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:
subject_token no es válido o está expirado.target_tenant solicitado, 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.