Guía del integrador

Integra Data Pilot Tax paso a paso

Este es el recorrido de la API para conectar un software integrador: crear la cuenta, registrar el emisor, obtener una API key, emitir facturas, consultar su estado y recibir webhooks.

URL base de los ejemplos: https://api.datapilotax.es

1 Alta y login

Paso 0 · Consigue tu acceso

Durante la beta las cuentas las damos de alta nosotros, una a una. Escríbenos a contacto@datapilotax.es y te creamos la cuenta y tu primera API key. A partir de ahí, todo lo que hay debajo lo puedes hacer tú solo.

Por eso POST /api/v1/auth/register responde 403 hoy: existe, pero está cerrado a propósito mientras dura la beta.

POST/api/v1/auth/register

Registrar una cuenta

Intenta crear una cuenta con email, contraseña y aceptación de los dos documentos.

Autenticación

Público

Cabeceras y parámetros

Content-Type: application/json

Ejemplo cURL

curl -i -X POST "https://api.datapilotax.es/api/v1/auth/register" \
  -H 'Content-Type: application/json' \
  -d '{
    "series": "F2026",
    "number": "0001",
    "issueDate": "2026-09-05",
    "issuer": {
      "taxIdNumber": "B12345674",
      "legalName": "INTEGRADOR SL",
      "address": {
        "address": "Calle Mayor 1",
        "postCode": "28001",
        "town": "Madrid",
        "province": "Madrid",
        "countryCode": "ESP"
      }
    },
    "receiver": {
      "taxIdNumber": "A58818501",
      "legalName": "CLIENTE DE PRUEBA SL",
      "address": {
        "address": "Gran Via 2",
        "postCode": "28013",
        "town": "Madrid",
        "province": "Madrid",
        "countryCode": "ESP"
      }
    },
    "lines": [
      {
        "description": "Servicios de desarrollo de software",
        "quantity": 1.00,
        "unitPriceWithoutTax": 100.00,
        "totalAmountWithoutTax": 100.00,
        "taxRate": 21.00
      }
    ],
    "totals": {
      "totalGrossAmount": 100.00,
      "totalTaxAmount": 21.00,
      "totalPayableAmount": 121.00
    }
  }'

Respuesta

403: Public registration is currently disabled.

email y password son obligatorios; la contraseña debe tener al menos 8 caracteres. acceptedTerms y acceptedPrivacy deben ser true. La validación puede devolver 400.

POST/api/v1/auth/login

Iniciar sesión

Autentica al usuario y devuelve un JWT. También establece la cookie HttpOnly AUTH_TOKEN.

Autenticación

Público

Cabeceras y parámetros

Content-Type: application/json

Ejemplo cURL

curl -i -X POST "https://api.datapilotax.es/api/v1/auth/login" \
  -H 'Content-Type: application/json' \
  -d '{"email":"integrador@example.com","password":"un-secreto-de-pruebas"}'

Respuesta

{ "token": "eyJ..." }

200; 400 por datos inválidos; 401 por credenciales incorrectas.

Para llamadas máquina a máquina se usa Authorization: Bearer <token> cuando el endpoint pide JWT.

GET/api/v1/auth/verify-email?token=...

Verificar el email

Consume el token del enlace de verificación y marca el email como verificado.

Autenticación

Público; la credencial es el parámetro token, no JWT

Cabeceras y parámetros

Sin cabeceras especiales

Ejemplo cURL

curl -i "https://api.datapilotax.es/api/v1/auth/verify-email?token=TOKEN_DEL_EMAIL"

Respuesta

Email verificado correctamente. Ya puedes usar tu cuenta.

200; 400 si el token es inválido, usado o caducado. Dura 24 horas y solo puede usarse una vez.
POST/api/v1/auth/resend-verification

Pedir otro email de verificación

Solicita el reenvío del enlace de verificación.

Autenticación

Público

Cabeceras y parámetros

Content-Type: application/json

Ejemplo cURL

curl -i -X POST "https://api.datapilotax.es/api/v1/auth/resend-verification" \
  -H 'Content-Type: application/json' \
  -d '{"email":"integrador@example.com"}'

Respuesta

Si esa direccion tiene una cuenta pendiente de verificar, recibiras un correo.

202 siempre; los datos inválidos pueden devolver 400.

2 Crear un emisor

POST/api/v1/issuers

Dar de alta el NIF emisor

Registra un NIF bajo la cuenta autenticada.

Autenticación

JWT Bearer

Cabeceras y parámetros

Authorization: Bearer <jwt>, Content-Type: application/json

Ejemplo cURL

curl -i -X POST "https://api.datapilotax.es/api/v1/issuers" \
  -H "Authorization: Bearer $JWT" \
  -H 'Content-Type: application/json' \
  -d '{"nif":"B12345674","corporateName":"INTEGRADOR SL"}'

Respuesta

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "nif": "B12345674",
  "corporateName": "INTEGRADOR SL",
  "hasCertificate": false,
  "modoVerifactu": true
}

201; 400 por datos o reglas de negocio, 401 sin credencial y 403 si no se puede acceder o aplicar el plan.

El NIF debe tener nueve caracteres alfanuméricos y su dígito de control debe ser correcto.

3 Sacar una API key

POST/api/v1/api-keys

Crear una API key

Crea una credencial para integrar el sistema máquina a máquina. Una API key no puede crear otra API key.

Autenticación

JWT Bearer

Cabeceras y parámetros

Authorization: Bearer <jwt>, Content-Type: application/json

Ejemplo cURL

curl -i -X POST "https://api.datapilotax.es/api/v1/api-keys" \
  -H "Authorization: Bearer $JWT" \
  -H 'Content-Type: application/json' \
  -d '{"name":"produccion"}'

Respuesta

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "plainTextKey": "tp_live_...",
  "name": "produccion",
  "lastFourChars": "abcd",
  "createdAt": "2026-09-05T12:00:00Z",
  "message": "Store this key safely. It will not be shown again."
}

201; 400 por petición inválida; 401 sin JWT válido.

plainTextKey solo se devuelve en esta respuesta. Guárdala: al listar las claves solo se devuelven los cuatro últimos caracteres.

4 Emitir una factura

POST/api/v1/facturae/json

Dar de alta una factura JSON

Encadena la factura en Veri*FACTU y devuelve un recibo mientras el envío a AEAT queda para el procesamiento posterior.

Autenticación

API key o JWT; el E2E usa API key

Cabeceras y parámetros

X-API-Key, X-Certificate-Password, opcionalmente Idempotency-Key, Content-Type: application/json

Ejemplo cURL

curl -i -X POST "https://api.datapilotax.es/api/v1/facturae/json" \
  -H "X-API-Key: $API_KEY" \
  -H 'X-Certificate-Password: clave-del-p12' \
  -H 'Idempotency-Key: 3f7c1a90-6b2e-4f18-9a55-0c1d2e3f4a5b' \
  -H 'Content-Type: application/json' \
  -d '{
    "series": "F2026", "number": "0001", "issueDate": "2026-09-05",
    "issuer": {"taxIdNumber": "B12345674", "legalName": "INTEGRADOR SL", "address": {"address":"Calle Mayor 1","postCode":"28001","town":"Madrid","province":"Madrid"}},
    "receiver": {"taxIdNumber": "A58818501", "legalName": "CLIENTE DE PRUEBA SL", "address": {"address":"Gran Via 2","postCode":"28013","town":"Madrid","province":"Madrid"}},
    "lines": [{"description":"Servicios de desarrollo de software","quantity":1.00,"unitPriceWithoutTax":100.00,"totalAmountWithoutTax":100.00,"taxRate":21.00}],
    "totals": {"totalGrossAmount":100.00,"totalTaxAmount":21.00,"totalPayableAmount":121.00}
  }'

Respuesta

{
  "id": "fe82b13d-56bf-4571-9ac6-ace0d1d38c97",
  "idEmisorFactura": "B99887762",
  "numSerieFactura": "0007",
  "fechaExpedicion": "2026-08-30",
  "tipo": "ALTA",
  "importeTotal": 121.00,
  "cuotaTotal": 21.00,
  "huella": "206D56E2C2CFB739A1...",
  "status": "READY_TO_SUBMIT",
  "errorReason": null,
  "attempts": 0,
  "createdAt": "2026-08-30T17:15:12",
  "aeat": null
}

201; 400, 401, 402, 409, 422, 429 o 500 según la causa.

Campos:currency y version son opcionales y toman EUR y 3.2.1. En cada línea, tipoImpuesto, claveRegimen, calificacion, causaExencion y recargoEquivalenciaRate son opcionales.

La cabecera Location apunta a /api/v1/registros/{id}. aeat es null hasta que haya un envío.

Idempotency-Key

Opcional. Si tu petición muere por un timeout y no sabes si llegó, reinténtala con la misma clave: te devolvemos la respuesta de la primera vez en lugar de emitir una segunda factura. Una clave por factura, máximo 64 caracteres, nunca vacía.

Qué recibes al reintentar

Misma clave, mismo cuerpo, la primera ya terminóLa misma respuesta, con la cabecera Idempotent-Replay: true. No se factura nada nuevo.
Misma clave, la primera sigue en curso409. Espera unos segundos y reinténtalo.
Misma clave, cuerpo distinto422. Esa clave ya se gastó con otra factura: usa una nueva.

Si el alta falló

Depende de de quién sea el problema:

  • Del dato que mandaste (400, 403, 404, 409): la clave se gasta. Reintentar lo mismo te devuelve ese mismo error sin ejecutar nada — que es lo que impide que aparezca una segunda factura. Corrige el dato y usa una clave nueva.
  • Nuestro o del entorno (402, 429, 500): la clave se libera, porque esa misma petición sí puede salir bien más tarde. Reintenta con la misma clave.

5 Consultar el estado

GET/api/v1/registros/{registroId}

Consultar un registro por su ID

Devuelve el estado de Data Pilot Tax y el acuse del último envío a AEAT.

Autenticación

API key o JWT autenticado

Cabeceras y parámetros

X-API-Key o Authorization: Bearer <jwt>

Ejemplo cURL

curl -i "https://api.datapilotax.es/api/v1/registros/$REGISTRO_ID" \
  -H "X-API-Key: $API_KEY"

Respuesta

Misma forma que la respuesta del alta; 200. aeat es null si nunca se envió. Cuando existe contiene csv, estadoEnvio, estadoRegistro, codigoError, descripcionError, enviadoEn y respuestaRecibidaEn. 401, 403, 404 o 500 según la causa.
GET/api/v1/issuers/{nif}/registros?page=0&size=20&status=...

Listar registros de un emisor

Lista los registros del NIF, del más nuevo al más antiguo.

Autenticación

API key o JWT autenticado

Cabeceras y parámetros

X-API-Key o Authorization: Bearer <jwt>; page, size y status son opcionales

Ejemplo cURL

curl -i "https://api.datapilotax.es/api/v1/issuers/B12345674/registros?page=0&size=20" \
  -H "X-API-Key: $API_KEY"

Respuesta

{ "content": [ ... ], "page": { "size": 20, "number": 0, "totalElements": 9, "totalPages": 2 } }

200; tamaño máximo 100; 400 si status no es válido; 401, 403 o 500 según la causa.

6 Recibir webhooks

Son avisos asíncronos sobre la respuesta de AEAT. Primero se registra una URL; después Data Pilot Tax encola y entrega los eventos.

POST/api/v1/webhooks

Registrar un webhook

Registra una URL HTTPS y los eventos a recibir.

Autenticación

API key o JWT

Cabeceras y parámetros

X-API-Key o Authorization: Bearer <jwt>, Content-Type: application/json

Ejemplo cURL

curl -i -X POST "https://api.datapilotax.es/api/v1/webhooks" \
  -H "X-API-Key: $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/hooks/taxpilot","events":["registro.accepted","registro.rejected"]}'

Respuesta

201; la respuesta incluye id, url, events, active, createdAt y secret. 400 si la URL no es HTTPS, no está permitida o hay un evento desconocido; 401 sin credencial.

Eventos válidos: registro.accepted, registro.accepted_with_errors y registro.rejected. El secreto solo se devuelve al crearlo. Si se pierde, hay que borrar el webhook y crear otro.

GET/api/v1/webhooks

Listar mis webhooks

Lista los webhooks de la cuenta.

Autenticación

API key o JWT

Cabeceras y parámetros

X-API-Key o Authorization: Bearer <jwt>

Ejemplo cURL

curl -i "https://api.datapilotax.es/api/v1/webhooks" \
  -H "X-API-Key: $API_KEY"

Respuesta

200 con una lista. Cada elemento contiene id, url, events, active y createdAt; secret es null y no vuelve a mostrarse.
DELETE/api/v1/webhooks/{id}

Borrar un webhook

Elimina un webhook de la cuenta.

Autenticación

API key o JWT

Cabeceras y parámetros

X-API-Key o Authorization: Bearer <jwt>

Ejemplo cURL

curl -i -X DELETE "https://api.datapilotax.es/api/v1/webhooks/$WEBHOOK_ID" \
  -H "X-API-Key: $API_KEY"

Respuesta

204 sin cuerpo; 401 sin credencial y 404 si no existe o pertenece a otra cuenta.

Petición que recibirá el integrador

Data Pilot Tax envía un POST JSON con Content-Type: application/json, X-TaxPilot-Event, X-TaxPilot-Delivery y X-TaxPilot-Signature: sha256=<hex>.

{
  "event": "registro.rejected",
  "occurredAt": "2026-09-05T12:00:00Z",
  "data": {
    "registroId": "fe82b13d-56bf-4571-9ac6-ace0d1d38c97",
    "idEmisorFactura": "B12345674",
    "numSerieFactura": "F2026/0001",
    "status": "REJECTED",
    "errorReason": "..."
  }
}

La firma es HMAC-SHA256 sobre el cuerpo recibido exactamente como bytes, usando el secreto. Una respuesta 2xx marca la entrega como correcta; cualquier otra respuesta o error de conexión se reintenta con espera creciente. La entrega puede estar desactivada por configuración; si no llega un aviso, consultar el estado sigue siendo la fuente comprobable.


Snippets por plataforma

Código listo para copiar y pegar. Pensado para desarrolladores que ya conocen su entorno — el script de FileMaker se parece a FileMaker, el VBA se parece a Access, el PHP se parece a PHP.

cURL — Terminal / Bash

macOS · Linux · Git Bash (Windows)

La forma más directa de probar la API. Ideal para scripts de automatización en servidores Linux, CI/CD pipelines o verificar que tu JSON es correcto antes de integrarlo en tu ERP. La opción -o factura.xml guarda el resultado directamente en disco.

bash
# ──────────────────────────────────────────────────────────────
#  Data Pilot Tax — Generar factura FacturaE / VeriFactu
#  Requisitos: curl 7.x+  |  Compatible macOS, Linux, Git Bash
# ──────────────────────────────────────────────────────────────

curl -X POST "https://api.datapilotax.es/api/v1/facturae/json" \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: txp_live_TU_CLAVE_AQUI" \
  -H "X-Certificate-Password: PIN_DE_TU_P12" \
  -o factura_firmada.xml \
  -d '{
    "series":    "F2025",
    "number":    "0001",
    "issueDate": "2025-06-15",
    "currency":  "EUR",
    "issuer": {
      "taxIdNumber":      "B12345678",
      "taxIdCountryCode": "ESP",
      "legalName":        "Mi Empresa SL",
      "address": {
        "address": "Calle Gran Vía 1", "postCode": "28013",
        "town": "Madrid", "province": "Madrid", "countryCode": "ESP"
      }
    },
    "receiver": {
      "taxIdNumber":      "A98765432",
      "taxIdCountryCode": "ESP",
      "legalName":        "Cliente SA",
      "address": {
        "address": "Diagonal 100", "postCode": "08019",
        "town": "Barcelona", "province": "Barcelona", "countryCode": "ESP"
      }
    },
    "lines": [{
      "description": "Servicio de consultoría",
      "quantity": 1,
      "unitPriceWithoutTax": 1500.00,
      "totalAmountWithoutTax": 1500.00,
      "taxRate": 21.0
    }],
    "totals": {
      "totalGrossAmount": 1500.00,
      "totalTaxAmount": 315.00,
      "totalPayableAmount": 1815.00
    }
  }'

# Respuesta 200 → el fichero factura_firmada.xml contiene el XML FacturaE listo
# Respuesta 4xx → revisar el JSON (campos requeridos marcados con * en Swagger)

📦 FileMaker Pro — VeriFactu y FacturaE

FileMaker Pro 19+ · FileMaker Server

Usa el paso de guion «Insertar desde URL» con las funciones JSON nativas de FileMaker (JSONSetElement). El script siguiente está estructurado como si lo vieras en el Espacio de trabajo de guiones de FileMaker — incluye los pasos, variables y la lógica condicional tal como los escribirías. Adapta los nombres de campo a tu solución.

Insertar desde URLJSONSetElementEstablecer VariablePatternCount✓ FacturaE 3.2.1✓ VeriFactu
FileMaker Script Workspace
╔══════════════════════════════════════════════════════════════════╗
║  Script FileMaker Pro: "Generar Factura FacturaE"               ║
║  Usando: Insertar desde URL  +  Funciones JSON nativas           ║
║  Probado en FileMaker Pro 19+ / FileMaker Server 19+             ║
╚══════════════════════════════════════════════════════════════════╝

# ── 1. Configuración (guardar en tabla "Config", no hardcodeada) ──
Establecer Variable [$url    ; "https://api.datapilotax.es/api/v1/facturae/json"]
Establecer Variable [$apikey ; Config::api_key]
Establecer Variable [$pin    ; Config::pin_certificado_p12]

# ── 2. Construir el JSON con funciones nativas de FileMaker ───────
Establecer Variable [$json ;
  JSONSetElement ( "{}" ;
    ["series"   ; Facturas::serie        ; JSONString] ;
    ["number"   ; Facturas::numero       ; JSONString] ;
    ["issueDate"; Text(Facturas::fecha;"YYYY-MM-DD") ; JSONString] ;
    ["currency" ; "EUR"                  ; JSONString] ;

    ["issuer.taxIdNumber"      ; Empresas::nif          ; JSONString] ;
    ["issuer.taxIdCountryCode" ; "ESP"                  ; JSONString] ;
    ["issuer.legalName"        ; Empresas::razon_social ; JSONString] ;
    ["issuer.address.address"  ; Empresas::direccion    ; JSONString] ;
    ["issuer.address.postCode" ; Empresas::cp           ; JSONString] ;
    ["issuer.address.town"     ; Empresas::ciudad       ; JSONString] ;
    ["issuer.address.province" ; Empresas::provincia    ; JSONString] ;
    ["issuer.address.countryCode"; "ESP"                ; JSONString] ;

    ["receiver.taxIdNumber"      ; Clientes::nif          ; JSONString] ;
    ["receiver.taxIdCountryCode" ; "ESP"                  ; JSONString] ;
    ["receiver.legalName"        ; Clientes::razon_social ; JSONString] ;
    ["receiver.address.address"  ; Clientes::direccion    ; JSONString] ;
    ["receiver.address.postCode" ; Clientes::cp           ; JSONString] ;
    ["receiver.address.town"     ; Clientes::ciudad       ; JSONString] ;
    ["receiver.address.province" ; Clientes::provincia    ; JSONString] ;
    ["receiver.address.countryCode"; "ESP"               ; JSONString] ;

    ["lines[0].description"           ; Lineas::concepto       ; JSONString] ;
    ["lines[0].quantity"              ; Lineas::cantidad        ; JSONNumber] ;
    ["lines[0].unitPriceWithoutTax"   ; Lineas::precio_unit     ; JSONNumber] ;
    ["lines[0].totalAmountWithoutTax" ; Lineas::base_imponible  ; JSONNumber] ;
    ["lines[0].taxRate"               ; Lineas::tipo_iva        ; JSONNumber] ;

    ["totals.totalGrossAmount"   ; Facturas::base_total   ; JSONNumber] ;
    ["totals.totalTaxAmount"     ; Facturas::cuota_iva    ; JSONNumber] ;
    ["totals.totalPayableAmount" ; Facturas::total_factura; JSONNumber]
  )
]

# ── 3. Llamada HTTP (Insertar desde URL) ──────────────────────────
Insertar desde URL [Seleccionar; Con diálogo: Desactivado;
  Destino: Facturas::xml_resultado;
  URL: $url;
  Verificar certificados SSL: Activado;
  Opciones cURL:
    "-X POST" &
    " --header "Content-Type: application/json"" &
    " --header "X-API-KEY: " & $apikey & """ &
    " --header "X-Certificate-Password: " & $pin & """ &
    " --data @$json"
]

# ── 4. Evaluar la respuesta ───────────────────────────────────────
Si [PatternCount(Facturas::xml_resultado ; "<Facturae") > 0]
  Establecer campo [Facturas::estado ; "FIRMADA"]
  Establecer campo [Facturas::fecha_firma ; Get(FechaActual)]
  Mostrar diálogo personalizado ["✅ Factura XML generada y almacenada correctamente."]
Si no
  Establecer campo [Facturas::estado ; "ERROR"]
  Establecer campo [Facturas::error_msg ; Facturas::xml_resultado]
  Mostrar diálogo personalizado ["❌ Error: " & Facturas::xml_resultado]
Fin Si

Para múltiples líneas de factura, genera el array JSON con un bucle Ir a registro / solicitud / página [Siguiente] y concatena cada elemento antes de la llamada HTTP.

📊 Microsoft Access — VBA y VeriFactu

Access 2016+ · VBA · MSXML2

Módulo VBA estándar para Microsoft Access. Usa MSXML2.ServerXMLHTTP.6.0 (disponible en Windows desde Office 2007). El código es un módulo real listo para pegar en Herramientas → Editor de Visual Basic → Módulo. Requiere activar en Herramientas → Referencias: Microsoft XML, v6.0.

MSXML2.ServerXMLHTTP.6.0TLS 1.2+DLookup Config✓ FacturaE 3.2.1✓ VeriFactu
VBA — modFacturaE.bas
'================================================================
'  Módulo: modFacturaE
'  Data Pilot Tax — Integración FacturaE / VeriFactu desde Access
'  Referencia COM requerida: Microsoft XML, v6.0  (MSXML2)
'  Herramientas > Referencias > marcar "Microsoft XML, v6.0"
'================================================================
Option Explicit

Public Sub GenerarFacturaFacturaE()

    '-- Configuración (mejor leerlo de tabla tConfig)
    Const API_URL   As String = "https://api.datapilotax.es/api/v1/facturae/json"
    Dim sApiKey     As String: sApiKey = DLookup("valor", "tConfig", "clave='API_KEY'")
    Dim sPinP12     As String: sPinP12 = InputBox("PIN del certificado P12:", "Firma Segura")
    If sPinP12 = "" Then Exit Sub   ' usuario canceló

    '-- Construir JSON (Access no tiene JSON nativo; usamos concatenación)
    Dim sSerie      As String: sSerie   = Forms!fFacturas!serie
    Dim sNumero     As String: sNumero  = Forms!fFacturas!numero
    Dim sFecha      As String: sFecha   = Format(Forms!fFacturas!fecha, "YYYY-MM-DD")

    Dim sJson As String
    sJson = "{" & _
        """series"":""" & sSerie & """," & _
        """number"":""" & sNumero & """," & _
        """issueDate"":""" & sFecha & """," & _
        """currency"":""EUR""," & _
        """issuer"":{" & _
            """taxIdNumber"":""" & Forms!fFacturas!nif_emisor & """," & _
            """taxIdCountryCode"":""ESP""," & _
            """legalName"":""" & Forms!fFacturas!razon_emisor & """," & _
            """address"":{" & _
                """address"":""" & Forms!fFacturas!dir_emisor & """," & _
                """postCode"":""" & Forms!fFacturas!cp_emisor & """," & _
                """town"":""" & Forms!fFacturas!ciudad_emisor & """," & _
                """province"":""" & Forms!fFacturas!prov_emisor & """," & _
                """countryCode"":""ESP""}}," & _
        """receiver"":{" & _
            """taxIdNumber"":""" & Forms!fFacturas!nif_receptor & """," & _
            """taxIdCountryCode"":""ESP""," & _
            """legalName"":""" & Forms!fFacturas!razon_receptor & """," & _
            """address"":{" & _
                """address"":""" & Forms!fFacturas!dir_receptor & """," & _
                """postCode"":""" & Forms!fFacturas!cp_receptor & """," & _
                """town"":""" & Forms!fFacturas!ciudad_receptor & """," & _
                """province"":""" & Forms!fFacturas!prov_receptor & """," & _
                """countryCode"":""ESP""}}," & _
        """lines"":[{" & _
            """description"":""" & Forms!fFacturas!concepto & """," & _
            """quantity"":" & Forms!fFacturas!cantidad & "," & _
            """unitPriceWithoutTax"":" & Forms!fFacturas!precio_unit & "," & _
            """totalAmountWithoutTax"":" & Forms!fFacturas!base_imp & "," & _
            """taxRate"":" & Forms!fFacturas!iva & "}]," & _
        """totals"":{" & _
            """totalGrossAmount"":" & Forms!fFacturas!base_total & "," & _
            """totalTaxAmount"":" & Forms!fFacturas!cuota_iva & "," & _
            """totalPayableAmount"":" & Forms!fFacturas!total_factura & "}" & _
    "}"

    '-- Petición HTTP con MSXML2 (soporta TLS 1.2+)
    Dim http As Object
    Set http = CreateObject("MSXML2.ServerXMLHTTP.6.0")

    On Error GoTo ErrHandler
    http.Open "POST", API_URL, False
    http.setRequestHeader "Content-Type", "application/json"
    http.setRequestHeader "X-API-KEY", sApiKey
    http.setRequestHeader "X-Certificate-Password", sPinP12
    http.send sJson

    '-- Evaluar respuesta
    If http.Status = 200 Then
        Dim sXml As String
        sXml = http.responseText

        '-- Guardar XML en disco
        Dim iFile As Integer: iFile = FreeFile
        Dim sRuta As String
        sRuta = CurrentProject.Path & "acturas" & sSerie & "-" & sNumero & ".xml"
        Open sRuta For Output As #iFile
            Print #iFile, sXml
        Close #iFile

        '-- Actualizar tabla
        CurrentDb.Execute "UPDATE tFacturas SET estado='FIRMADA', ruta_xml='" & _
            sRuta & "' WHERE serie='" & sSerie & "' AND numero='" & sNumero & "'"

        MsgBox "✅ Factura generada: " & sRuta, vbInformation
    Else
        MsgBox "❌ Error " & http.Status & ": " & http.responseText, vbCritical
    End If

    Set http = Nothing
    Exit Sub

ErrHandler:
    MsgBox "Error de conexión: " & Err.Description, vbCritical
    Set http = Nothing
End Sub

Si obtienes error de TLS en Windows antiguo, ejecuta en PowerShell: Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' -Name 'Enabled' -Value 1

🐘 PHP — WordPress, WooCommerce, Laravel

PHP 7.4+ · ext-curl

Script PHP puro con curl (disponible en prácticamente todos los hostings). Funciona en WordPress / WooCommerce (como plugin o función en functions.php), Laravel, Symfony, CodeIgniter o PHP plano. Las credenciales se leen de variables de entorno para no quedar hardcodeadas en el código.

ext-curlgetenv()json_encode()WordPress compatible✓ FacturaE 3.2.1✓ VeriFactu
datapilot-tax.php
<?php
/**
 * Data Pilot Tax — Generar factura FacturaE / VeriFactu
 * Compatibilidad: PHP 7.4+  |  WordPress, WooCommerce, Laravel, puro
 * Extensión requerida: ext-curl (activa en casi todos los hostings)
 */

// ── Configuración ─────────────────────────────────────────────────────────────
define('DPT_API_URL',  'https://api.datapilotax.es/api/v1/facturae/json');
define('DPT_API_KEY',  getenv('DPT_API_KEY') ?: 'txp_live_TU_CLAVE_AQUI');
define('DPT_PIN_P12',  getenv('DPT_PIN_P12')  ?: 'PIN_DE_TU_CERTIFICADO');
// ⚠️  Nunca hardcodear credenciales. Usa variables de entorno o wp-config.php

// ── Payload — estructura exacta requerida por el backend ──────────────────────
$payload = [
    'series'    => 'F2025',
    'number'    => '0001',
    'issueDate' => '2025-06-15',   // formato YYYY-MM-DD
    'currency'  => 'EUR',
    'version'   => '3.2.1',

    'issuer' => [
        'taxIdNumber'      => 'B12345678',   // NIF/CIF emisor — 9 chars
        'taxIdCountryCode' => 'ESP',
        'legalName'        => 'Mi Empresa SL',
        'address' => [
            'address'     => 'Calle Gran Vía 1',
            'postCode'    => '28013',
            'town'        => 'Madrid',
            'province'    => 'Madrid',
            'countryCode' => 'ESP',
        ],
    ],

    'receiver' => [
        'taxIdNumber'      => 'A98765432',   // NIF/CIF receptor
        'taxIdCountryCode' => 'ESP',
        'legalName'        => 'Cliente Ejemplo SA',
        'address' => [
            'address'     => 'Avenida Diagonal 100',
            'postCode'    => '08019',
            'town'        => 'Barcelona',
            'province'    => 'Barcelona',
            'countryCode' => 'ESP',
        ],
    ],

    // ⚠️ Clave correcta: "lines" (NO "items")
    'lines' => [
        [
            'description'           => 'Desarrollo módulo ERP integración FacturaE',
            'quantity'              => 1,
            'unitPriceWithoutTax'   => 1500.00,
            'totalAmountWithoutTax' => 1500.00,   // quantity * unitPrice
            'taxRate'               => 21.0,
        ],
    ],

    'totals' => [
        'totalGrossAmount'   => 1500.00,
        'totalTaxAmount'     =>  315.00,   // base * (taxRate/100)
        'totalPayableAmount' => 1815.00,   // gross + tax
    ],
];

// ── Petición cURL ──────────────────────────────────────────────────────────────
$ch = curl_init(DPT_API_URL);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'X-API-KEY: '              . DPT_API_KEY,
        'X-Certificate-Password: ' . DPT_PIN_P12,
    ],
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_SSL_VERIFYPEER => true,   // Nunca deshabilitar en producción
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr  = curl_error($ch);
curl_close($ch);

// ── Manejar resultado ──────────────────────────────────────────────────────────
if ($curlErr) {
    error_log('[DataPilotTax] cURL error: ' . $curlErr);
    throw new RuntimeException('Error de red: ' . $curlErr);
}

if ($httpCode === 200) {
    // $response contiene el XML FacturaE firmado
    $filename = 'factura_' . $payload['series'] . '_' . $payload['number'] . '.xml';
    file_put_contents(__DIR__ . '/facturas/' . $filename, $response);

    // Para WooCommerce / WordPress puedes adjuntarlo al pedido:
    // update_post_meta($order_id, '_factura_xml_path', $filename);

    echo "✅ Factura generada: {$filename}";
    return $response;
} else {
    error_log('[DataPilotTax] Error ' . $httpCode . ': ' . $response);
    throw new RuntimeException("Error API ({$httpCode}): {$response}");
}
?>
O

Opcional: certificado para FacturaE firmada

Este paso no forma parte del camino normal. La mayoría de integradores no lo necesita. Para declarar ante la AEAT (Veri*FACTU) no hace falta tu certificado: el envío lo firma Data Pilot Tax con el suyo propio.

Tu certificado solo sirve para firmar la FacturaE con XAdES. Incluso entonces es opcional: sin certificado, la FacturaE se genera igual, sin firmar, y la petición no falla. Si no sabes si lo necesitas, sáltate esta sección.

POST/api/v1/issuers/{nif}/certificate

Subir un certificado P12

Sube el certificado P12 cifrado asociado al NIF.

Autenticación

JWT Bearer

Cabeceras y parámetros

Authorization: Bearer <jwt>, Content-Type: multipart/form-data, campo file

Ejemplo cURL

curl -i -X POST "https://api.datapilotax.es/api/v1/issuers/B12345674/certificate" \
  -H "Authorization: Bearer $JWT" \
  -F 'file=@certificado.p12'

Respuesta

Certificate uploaded and encrypted securely for NIF: B12345674

200; 400 si falla la subida o el certificado, 401 sin credencial y 403 si el NIF no pertenece a la cuenta.

La contraseña se envía después, al emitir, mediante X-Certificate-Password. Si subes un certificado y emites sin esa cabecera, la petición falla con 400. No lo subas «por si acaso».

Respuestas de error

Las respuestas de error generadas por la API tienen esta forma general:

{
  "status": 400,
  "error": "Bad Request",
  "message": "...",
  "timestamp": "2026-09-05T12:00:00"
}

🛡️ Seguridad e Infraestructura

🏢

Datos en España

Servidores en Oracle Cloud Infrastructure — Región Madrid (eu-madrid-1). Tus datos nunca salen del territorio nacional.

🔐

Certificados cifrados en reposo

Los certificados P12 se almacenan cifrados (AES-256). La contraseña del certificado jamás se persiste — se usa solo en memoria durante la petición.

🔒

Comunicación TLS 1.3

Todas las peticiones van cifradas con TLS 1.3. Las API Keys viajan en cabecera HTTP (nunca en URL). Tokens JWT con expiración y rotación.

🇪🇺

RGPD / LOPD compliant

Tratamiento de datos conforme al Reglamento General de Protección de Datos europeo y la LOPDGDD española. Datos fiscales retenidos según Ley General Tributaria.

📋

Normativa española

XML generado conforme a FacturaE 3.2.1 y VeriFactu (RD 1007/2023). Firma XADES-EPES con el certificado delegado del emisor.

🚫

Sin instalaciones

Cero dependencias en tu sistema. No necesitas Java, librerías de firma, certificados locales ni acceso al FNMT. Solo HTTP.

👋 ¿Por qué usar Data Pilot Tax?

Si programas en FileMaker Pro, Microsoft Access, Visual Basic o PHP, implementar la firma criptográfica XADES-EPES de un XML FacturaE son cientos de horas de trabajo especializado que tu sistema legacy no está diseñado para asumir.

Data Pilot Tax resuelve esto en 1 petición HTTP: envías el JSON con los datos de la factura, la API valida el formato, genera el XML FacturaE, lo firma con tu certificado delegado almacenado, lo guarda y te lo devuelve. Tu sistema legacy solo necesita saber hacer una llamada HTTP POST.

¿Listo para integrar FacturaE en tu sistema?

Accede al Dashboard, genera tu API Key y envía tu primera factura en minutos. Sin contratos, sin tarjeta en el plan gratuito.