Saltar al contenido
Developer Docs

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:

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

CampoObligatorioDescripción
taxIdIdentificación fiscal de la organización cliente.
typeCOMPANY o SELF_EMPLOYER.
legalNameSí, si type es COMPANYRazón social de la organización cliente. Ignorado para autónomos.
name y lastNameSí, si type es SELF_EMPLOYERNombre y apellidos del autónomo. Ignorados para empresas.
tradeNameNoNombre comercial, si difiere de la razón social.
activityCodeCódigo de actividad de la organización, válido para el país del cliente. Consulta los valores válidos en Actividades empresariales.
fiscalAddressDomicilio fiscal de la organización cliente. Los códigos de país, estado, ciudad y postal se obtienen de Localizaciones.
collaboratorTypeRol con el que actúas sobre este cliente: Partner o Agente.
usersExactamente un usuario inicial del cliente, que actuará como su administrador.

Cada entrada de users tiene esta forma:

CampoObligatorioDescripción
nameNombre del usuario inicial.
lastNameApellidos del usuario inicial.
emailCorreo electrónico. Recibe una invitación a Zertiban.
roleDebe ser 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 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"
      }
    ]
  }'
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 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"]
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 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();
java
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):

json
{
  "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 cabecera x-tenant-id y el claim de tenant del token no coinciden entre sí.
  • 404 — el activityCode no es válido para el país del cliente.
  • 409 — ya existe una organización con el mismo taxId en Zertiban, o el collaboratorType declarado 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).

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();

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ámetroObligatorioDescripción
grant_typeurn:ietf:params:oauth:grant-type:token-exchange.
subject_tokenToken propio del colaborador obtenido en el paso 1.
subject_token_typeurn:ietf:params:oauth:token-type:access_token.
target_tenantParámetro específico de Zertiban (fuera de RFC 8693). UUID de la organización cliente destino.
requested_token_typeNoPor defecto 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();

El token delegado

El paso 2 devuelve un TokenResponse estándar de OAuth2:

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

El access_token devuelto tiene estas particularidades:

CaracterísticaValor
sub / tenant_idUUID de la organización cliente (no del colaborador)
Claim actEstructura anidada que identifica al colaborador que actúa (ver más abajo)
rolesRoles del colaborador sobre el cliente, derivados del mandato
authoritiesAutoridades otorgadas al colaborador sobre el cliente, derivadas del mandato
client_idclientId del colaborador (el que se autenticó en el paso 1)
expires_in900 s (vida corta — renuévalo antes de que caduque)

El claim act es un objeto con la siguiente forma:

Campo del actValor
subclientId del colaborador (el que se autenticó en el paso 1)
tenant_idUUID de la organización colaboradora (Partner o Agente)
collaborator_typePARTNER o AGENT

Ejemplo del payload JWT decodificado:

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"
}

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 (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.
  • 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:

http
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_token no es válido o está expirado.
  • No existe un mandato activo entre el colaborador y el 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.