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). Sabrás que lo está por el webhook BUSINESS_ACTIVATED; ver Cuándo se activa el cliente.
  • 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
taxIdSíIdentificación fiscal de la organización cliente.
typeSíCOMPANY 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.
activityCodeNoCó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).
fiscalAddressSíDomicilio fiscal de la organización cliente. Los códigos de país, estado, ciudad y postal se obtienen de Localizaciones.
collaboratorTypeSíRol con el que actúas sobre este cliente: Partner o Agente.
productsSí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.
usersSíExactamente un usuario inicial del cliente, que actuará como su administrador.

Cada entrada de users tiene esta forma:

CampoObligatorioDescripción
nameSíNombre del usuario inicial.
lastNameSíApellidos del usuario inicial.
emailSíCorreo electrónico. Recibe una invitación a Zertiban.
roleSíDebe ser ADMINISTRATOR.

Enviar o no 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:

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

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.",
    "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):

json
{
  "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.

Cuándo se activa el cliente ​

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.

Errores del alta ​

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

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_typeSíurn:ietf:params:oauth:grant-type:token-exchange.
subject_tokenSíToken propio del colaborador obtenido en el paso 1.
subject_token_typeSíurn:ietf:params:oauth:token-type:access_token.
target_tenantSíPará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.