En esta guía vamos a usar la API de Factura SDK para emitir una Factura B de prueba en sandbox y descargar su PDF. Todo es simulado y nada llega a ARCA. El recorrido es el mismo que en producción: access token, emisor, certificado, punto de venta, verificación, webhook y emisión.
Nota: Todos los pasos aqui presentados (a excepción de emitir comprobante) pueden hacerse tambien desde el dashboard de Factura SDK.
1. Creá el proyecto y un access token
Entrá a app.afipsdk.com, creá un proyecto nuevo de tipo Factura SDK y, en Configuración → Access tokens, creá un token del ambiente dev. Se muestra una sola vez: copialo. Empieza con fsk_dev_; en los ejemplos lo vas a ver como <ACCESS_TOKEN>.
2. Creá el emisor
El emisor es el CUIT que factura; acá usamos 20111111112. Sus datos se imprimirán en el comprobante. sales_point es el punto de venta por el que van a salir sus comprobantes: usamos el 1.
const response = await fetch('https://app.afipsdk.com/api/v1/issuers', {
method: 'POST',
headers: {
'Authorization': 'Bearer <ACCESS_TOKEN>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
tax_id: '20111111112',
business_name: 'Emisor de prueba',
address: 'Av. Siempre Viva 742, CABA',
vat_condition: 1,
gross_income: '20111111112',
activity_start_date: '2020-01-01',
sales_point: 1
})
});
const issuer = await response.json();
La respuesta es 201 con el emisor en estado pending: existe, pero todavía no emite. Guardá el id, lo vas a usar en los pasos siguientes.
{
"id": "0f8f3c2a-5b1e-4c7d-9a2e-6d4b8e1f2a3c",
"tax_id": "20111111112",
"business_name": "Emisor de prueba",
"address": "Av. Siempre Viva 742, CABA",
"vat_condition": 1,
"gross_income": "20111111112",
"activity_start_date": "2020-01-01",
"sales_point": 1,
"certificate_id": null,
"status": "pending",
"verified_at": null,
"created_at": "2026-10-14T14:00:00.000Z",
"updated_at": "2026-10-14T14:00:00.000Z"
}
3. Dale un certificado
ARCA exige firmar cada comprobante con un certificado digital. En sandbox el certificado es simulado: se carga con los valores VALID_CERT y VALID_KEY, y nunca firma nada.
const response = await fetch(`https://app.afipsdk.com/api/v1/issuers/${issuer.id}/certificate`, {
method: 'POST',
headers: {
'Authorization': 'Bearer <ACCESS_TOKEN>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
cert: 'VALID_CERT',
key: 'VALID_KEY'
})
});
const certificate = await response.json();
La respuesta es 201 con el certificado, ya asignado al emisor (200 si ese mismo certificado ya estaba cargado):
{
"id": "c3a1e7d9-8b2f-4e6a-a1d4-5f7b9c0e2d31",
"tax_id": "20111111112",
"alias": "facturasdk",
"source": "manual",
"serial": null,
"expiration": "2028-10-14T14:02:00.000Z",
"status": "active",
"created_at": "2026-10-14T14:02:00.000Z",
"updated_at": "2026-10-14T14:02:00.000Z",
"issuer_id": "0f8f3c2a-5b1e-4c7d-9a2e-6d4b8e1f2a3c",
"delegation": "own"
}
Con un certificado real mandás el .crt y la .key, o dejás que Factura SDK lo cree en ARCA con la clave fiscal del emisor (source: "automation"). Todo el detalle está en Certificados.
4. Confirmá el punto de venta
Al emisor ya le dimos el punto de venta 1 al crearlo. Tiene que ser un punto de venta del CUIT habilitado para facturación electrónica por web service. Si no sabés cuál es, Factura SDK lista los de web services con la clave fiscal del emisor, y si no tiene ninguno lo crea:
const response = await fetch('https://app.afipsdk.com/api/v1/issuers/sales-points', {
method: 'POST',
headers: {
'Authorization': 'Bearer <ACCESS_TOKEN>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
tax_id: '20111111112',
username: '20111111112',
password: '<CLAVE_FISCAL>'
})
});
const { automation_id } = await response.json();
Es una Automatización. En sandbox termina al instante, con un usuario y una clave cualesquiera de al menos 4 caracteres (salvo INVALID_PASSWORD, que simula una clave fiscal incorrecta), y devuelve dos puntos de venta simulados más el número que tendría uno nuevo:
{
"id": "a9e2c4b6-1d3f-4a5b-8c7d-2e4f6a8b0c1d",
"status": "complete",
"data": {
"tax_id": "20111111112",
"sales_points": [
{ "number": 1, "system": "RECE1 - FACTURA ELECTRONICA - WEB SERVICES (sandbox)", "display_name": "Punto de venta simulado 1", "address": null, "blocked": false, "deactivated": false },
{ "number": 2, "system": "RECE1 - FACTURA ELECTRONICA - WEB SERVICES (sandbox)", "display_name": "Punto de venta simulado 2", "address": null, "blocked": false, "deactivated": false }
],
"next_number": 3
}
}
Para cambiar el punto de venta del emisor, PUT /v1/issuers/:id con sales_point. Más en Emisores.
5. Verificá el emisor
Un emisor pasa a active cuando se verifica: Factura SDK emite, con su certificado y su punto de venta, una factura de $1 y la nota de crédito que la anula. Si ARCA aprueba las dos, el emisor queda listo para emitir.
const response = await fetch(`https://app.afipsdk.com/api/v1/issuers/${issuer.id}/verify`, {
method: 'POST',
headers: {
'Authorization': 'Bearer <ACCESS_TOKEN>'
}
});
const { automation_id } = await response.json();
La respuesta es 202 con el automation_id. Consultá el resultado:
const response = await fetch(`https://app.afipsdk.com/api/v1/automations/${automation_id}`, {
headers: {
'Authorization': 'Bearer <ACCESS_TOKEN>'
}
});
const automation = await response.json();
console.log(automation.status, automation.data);
Mientras corre, responde 200 con status: "in_process". Cuando termina, status: "complete" y el emisor ya está active:
{
"id": "a9e2c4b6-1d3f-4a5b-8c7d-2e4f6a8b0c1d",
"status": "complete",
"data": {
"verified": true,
"issuer": {
"id": "0f8f3c2a-5b1e-4c7d-9a2e-6d4b8e1f2a3c",
"tax_id": "20111111112",
"status": "active",
"verified_at": "2026-10-14T14:03:00.000Z"
},
"invoice": { "id": "…", "verification": true, "status": "approved", "invoice_type": 6, "number": 1, "total_amount": 1, "...": "..." },
"credit_note": { "id": "…", "verification": true, "status": "approved", "invoice_type": 8, "number": 1, "total_amount": 1, "...": "..." }
}
}
Si alguno de los dos comprobantes no se aprueba, la respuesta es 400 con status: "error" y el motivo en data.errors. Corregí lo que falló y volvé a verificar. Más en Emisores.
6. Configurá el webhook
La URL tiene que ser pública: localhost no sirve. Para esta prueba podés usar una URL temporal de un servicio como webhook.site o exponer tu proyecto local con un túnel con ngrok o Cloudflare Tunnel.
const response = await fetch('https://app.afipsdk.com/api/v1/webhook-endpoints', {
method: 'POST',
headers: {
'Authorization': 'Bearer <ACCESS_TOKEN>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://api.tusistema.com/webhooks/facturasdk'
})
});
const endpoint = await response.json();
console.log(endpoint.secret); // Guardalo: no se vuelve a mostrar
La respuesta es 201 con el endpoint y su secret. Guardalo: es la única vez que lo vas a ver, y es con lo que vas a verificar la firma de cada evento recibido desde Factura SDK.
{
"id": "5d2b8f1c-9a3e-4c7b-b6d1-0e8f2a4c6b9d",
"url": "https://api.tusistema.com/webhooks/facturasdk",
"status": "active",
"created_at": "2026-10-14T14:04:00.000Z",
"updated_at": "2026-10-14T14:04:00.000Z",
"secret": "whsec_3fQ9kLm2Zx8vB1nC4pR7tY0uW6aE5dG3hJ9sK2lM8oP"
}
Tu endpoint tiene que responder 2xx en menos de 10 segundos; si no, Factura SDK reintenta. Para comprobar que la URL responde antes de emitir, podes enviar un evento de prueba.
7. Emití la factura
Vamos a emitir una Factura B (invoice_type 6) a un consumidor final, por un producto de $1.210 con IVA 21% incluido. El emisor se indica por su CUIT (issuer_cuit) y el punto de venta lo toma del valor cargado previamente en el emisor.
El header Idempotency-Key es opcional, pero conviene mandarlo siempre con el identificador de la venta en tu sistema: si la solicitud se corta y lo repetís con la misma clave, recibís el mismo comprobante en vez de emitir otro.
const response = await fetch('https://app.afipsdk.com/api/v1/invoices', {
method: 'POST',
headers: {
'Authorization': 'Bearer <ACCESS_TOKEN>',
'Content-Type': 'application/json',
'Idempotency-Key': 'venta-0001'
},
body: JSON.stringify({
issuer_cuit: '20111111112',
invoice_type: 6,
concept: 1,
receiver: {
doc_type: 96,
doc_number: '12345678',
vat_condition: 5,
name: 'Juan Pérez',
address: 'Calle Falsa 123, CABA',
email: '[email protected]'
},
amounts: {
net: 1000,
vat: 210,
total: 1210
},
vat_items: [
{ id: 5, base: 1000, amount: 210 }
],
items: [
{ description: 'Teclado inalámbrico', quantity: 1, unit_price: 1210, subtotal: 1210 }
],
sale_condition: 'Contado'
})
});
const invoice = await response.json();
console.log(response.status, invoice.status); // 202 queued
Factura SDK responde 202 enseguida con el comprobante en queued: ya está recibido y en cola, y la autorización en ARCA ocurre en segundo plano, en segundos. Guardá el id.
{
"id": "7c1d9e4b-2f3a-4b8c-9d0e-1a2b3c4d5e6f",
"idempotency_key": "venta-0001",
"verification": false,
"status": "queued",
"invoice_type": 6,
"sales_point": 1,
"number": null,
"invoice_date": null,
"total_amount": 1210,
"currency": "PES",
"cae": null,
"cae_expiration": null,
"payload": { "...": "los datos que mandaste, normalizados" },
"observations": null,
"errors": null,
"last_error": null,
"attempts": 0,
"next_retry_at": null,
"created_at": "2026-10-14T14:05:00.000Z",
"updated_at": "2026-10-14T14:05:00.000Z",
"processed_at": null,
"pdf_generated_at": null,
"email_status": null,
"email_sent_at": null,
"issuer": {
"id": "0f8f3c2a-5b1e-4c7d-9a2e-6d4b8e1f2a3c",
"tax_id": "20111111112",
"business_name": "Emisor de prueba",
"vat_condition": 1
}
}
El resultado llega a tu webhook
Factura SDK le hace un POST a tu URL con un evento: invoice.approved, invoice.rejected o invoice.failed, y en data el comprobante completo. Queda aprobado, salvo que fuerces otro resultado (más abajo), con un CAE de prueba de 14 dígitos, su vencimiento, el número asignado y el pdf_url.
{
"event_id": "evt_9f1e2d3c4b5a69788a9b0c1d2e3f4a5b",
"type": "invoice.approved",
"created": "2026-10-14T14:05:04.000Z",
"data": {
"id": "7c1d9e4b-2f3a-4b8c-9d0e-1a2b3c4d5e6f",
"idempotency_key": "venta-0001",
"verification": false,
"status": "approved",
"invoice_type": 6,
"sales_point": 1,
"number": 2,
"invoice_date": "2026-10-14",
"total_amount": 1210,
"currency": "PES",
"cae": "76412345678901",
"cae_expiration": "2026-10-24",
"payload": { "...": "los datos que mandaste, normalizados" },
"observations": null,
"errors": null,
"last_error": null,
"attempts": 1,
"next_retry_at": null,
"created_at": "2026-10-14T14:05:00.000Z",
"updated_at": "2026-10-14T14:05:03.000Z",
"processed_at": "2026-10-14T14:05:03.000Z",
"pdf_url": "https://app.afipsdk.com/api/v1/invoices/7c1d9e4b-2f3a-4b8c-9d0e-1a2b3c4d5e6f/pdf?token=kX3v9Qw2LmN8pR5tY7uZ1aB4cD6eF8gH0jK2lM4nO6p",
"pdf_generated_at": null,
"arca_request_url": "https://app.afipsdk.com/api/v1/invoices/7c1d9e4b-2f3a-4b8c-9d0e-1a2b3c4d5e6f/arca-request?token=kX3v9Qw2LmN8pR5tY7uZ1aB4cD6eF8gH0jK2lM4nO6p",
"arca_response_url": "https://app.afipsdk.com/api/v1/invoices/7c1d9e4b-2f3a-4b8c-9d0e-1a2b3c4d5e6f/arca-response?token=kX3v9Qw2LmN8pR5tY7uZ1aB4cD6eF8gH0jK2lM4nO6p",
"email_status": null,
"email_sent_at": null,
"issuer": {
"id": "0f8f3c2a-5b1e-4c7d-9a2e-6d4b8e1f2a3c",
"tax_id": "20111111112",
"business_name": "Emisor de prueba",
"vat_condition": 1
}
}
}
Cada evento viene firmado: verificá la firma con el secret del endpoint antes de procesarlo y respondé 200 enseguida. Los ejemplos están en Webhooks. Si en algún momento necesitás el estado actual del comprobante, GET /v1/invoices/:id es la fuente de verdad.
Como mandamos receiver.email, Factura SDK genera el PDF y se lo envía al receptor apenas se aprueba.
Si tu sistema necesita el resultado en la misma llamada, mandá sync: true en la solicitud: Factura SDK emite dentro de la solicitud y responde 200 con el comprobante en su estado final, sin webhook. Si ARCA no contesta dentro del tiempo de la solicitud, el comprobante queda failed y repetís la solicitud con la misma Idempotency-Key. Todo el detalle está en Comprobantes.
Simulá rechazos y errores
Para probar cómo responde tu integración ante cada resultado, podés forzarlo con el campo sandbox.result:
{
"sandbox": { "result": "rejected" }
}
sandbox.result | Qué simula |
|---|---|
approved | Aprobado. Es lo que pasa si no mandás nada. |
approved_with_observations | Aprobado (status: "approved") con una observación de prueba en observations. |
rejected | Rechazado, con el motivo en errors. |
error | Un fallo técnico. Por defecto el comprobante se reintenta en segundo plano, como en producción; con sync: true queda failed en el momento. |
El resultado llega a tu webhook como un evento real: invoice.rejected para el rechazo, invoice.failed cuando se agotan los reintentos del error. Fuera del sandbox el campo se ignora.
8. Descargá el PDF
Todo comprobante aprobado trae pdf_url: una URL pública y fija, que podés guardar en tu sistema o mandarle al cliente. La URL muestra el PDF; con ?download=true lo descarga.