Saltar al contenido
Tarifa anual desde S/ 37 al mes + IGV. Pide tu demo sin costo. Reserva tu demo
Sistema Facturación Perú
Menú del portal

Socios y conexión

Conectar con Sistema Facturación Perú

Pon un botón en tu sistema para que tus clientes conecten su cuenta de Sistema Facturación Perú sin copiar claves: OAuth 2.0 con PKCE, ventana emergente y tokens.

Actualizado el

Si tu sistema (ERP, tienda, app) tiene clientes que ya usan Sistema Facturación Perú, ponles un botón «Conectar con Sistema Facturación Perú». El cliente escribe su RUC, entra con su usuario de siempre y pulsa «Permitir»: tú recibes un token para emitir con su cuenta, sin pedirle claves de API y sin que su contraseña pase por tu sistema.

  • Solo conecta una cuenta que ya existe: nunca crea empresas ni usuarios. Para dar de alta empresas está la API de socios.
  • Es gratis para tu sistema. La empresa paga su plan de siempre.
  • Es OAuth 2.0 con authorization code y PKCE S256 obligatorio (RFC 6749, 7636, 7009 y 9207).
  1. Tu servidor genera state y code_verifier, los guarda en la sesión del usuario y arma la URL de /conectar.
  2. Tu botón abre esa URL en una ventana emergente, con nuestra dirección visible.
  3. El usuario escribe el RUC de su empresa, entra con su correo y contraseña en el subdominio de su empresa (con su segundo factor, si lo tiene) y pulsa «Permitir».
  4. Volvemos a tu redirect_uri con code, state e iss.
  5. Tu servidor comprueba el state y canjea el código en POST /oauth/token.
  6. Emites en la API v1 de esa empresa con el token de acceso, y lo renuevas cada hora.

1. Registra tu aplicación

Escríbenos con el nombre y el logo de tu sistema, tu web, un correo de soporte, tus direcciones de retorno (redirect_uri exactas: solo https://, o http://localhost y http://127.0.0.1 para desarrollar) y los permisos que vas a pedir. Te damos:

  • client_id (sfp_app_…), que es público;
  • client_secret (sfp_secret_…), que se muestra una sola vez: guárdalo en tu servidor, nunca en el navegador;
  • si nos das una URL de webhook, su secreto whsec_… para verificar el aviso conexion.revocada.

Sin ese registro no hay botón: así nadie se hace pasar por tu aplicación.

2. El botón y la ventana emergente

No metas nuestro inicio de sesión dentro de un <iframe>: el navegador bloquea las cookies, tu usuario no puede ver que escribe su contraseña en nuestro sitio y nuestras pantallas responden X-Frame-Options: DENY. Abre una ventana emergente (o redirige la página).

<button id="sfp-conectar" type="button">Conectar con Sistema Facturación Perú</button>
<script>
// 1. Tu servidor genera state y code_verifier, los guarda en la sesión y devuelve la URL:
//    GET /sfp/conectar-url → { "url": "https://api.sistemafacturacionperu.com/conectar?..." }
document.getElementById('sfp-conectar').addEventListener('click', async () => {
  const { url } = await (await fetch('/sfp/conectar-url', { credentials: 'same-origin' })).json();
  const w = 520, h = 680;
  const popup = window.open(url, 'sfp-conectar',
    `width=${w},height=${h},left=${(screen.width - w) / 2},top=${(screen.height - h) / 2}`);
  if (!popup) { window.location.href = url; return; }   // Si bloquean la ventana, redirige.

  // 3. Tu página de vuelta (redirect_uri) termina el canje en tu servidor y avisa con postMessage.
  window.addEventListener('message', (e) => {
    if (e.origin !== window.location.origin || e.data?.tipo !== 'sfp-conectado') return;
    popup.close();
    location.reload();   // Ya está conectado: muestra «Conectado · RUC …».
  });
});
</script>

<!-- En tu redirect_uri (https://tusistema.com/sfp/vuelta), después de canjear el código en el servidor: -->
<script>
  if (window.opener) { window.opener.postMessage({ tipo: 'sfp-conectado' }, window.location.origin); window.close(); }
  else { window.location.href = '/configuracion/facturacion'; }
</script>

La URL de la ventana:

https://api.sistemafacturacionperu.com/conectar
  ?response_type=code
  &client_id=sfp_app_…
  &redirect_uri=https%3A%2F%2Ftusistema.com%2Fsfp%2Fvuelta
  &state=<al azar, guardado en la sesión>
  &code_challenge=<BASE64URL(SHA256(code_verifier))>
  &code_challenge_method=S256
  &scope=comprobantes.emitir%20comprobantes.leer%20consultas

Así generas el code_verifier, el code_challenge y el state:

import { randomBytes, createHash } from 'node:crypto';

const b64url = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const codeVerifier = b64url(randomBytes(48)); // 64 caracteres
const codeChallenge = b64url(createHash('sha256').update(codeVerifier).digest());
const state = b64url(randomBytes(24));
// Guarda codeVerifier y state en la sesión del usuario.
<?php
$b64url = fn (string $bin) => rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
$codeVerifier = $b64url(random_bytes(48));
$codeChallenge = $b64url(hash('sha256', $codeVerifier, true));
$state = $b64url(random_bytes(24));
$_SESSION['sfp'] = ['verifier' => $codeVerifier, 'state' => $state];

$url = 'https://api.sistemafacturacionperu.com/conectar?' . http_build_query([
    'response_type' => 'code',
    'client_id' => getenv('SFP_APP_CLIENT_ID'),
    'redirect_uri' => 'https://tusistema.com/sfp/vuelta',
    'state' => $state,
    'code_challenge' => $codeChallenge,
    'code_challenge_method' => 'S256',
    'scope' => 'comprobantes.emitir comprobantes.leer consultas',
], '', '&', PHP_QUERY_RFC3986);
import base64, hashlib, secrets
from urllib.parse import urlencode

b64url = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
code_verifier = b64url(secrets.token_bytes(48))
code_challenge = b64url(hashlib.sha256(code_verifier.encode()).digest())
state = b64url(secrets.token_bytes(24))
# Guarda code_verifier y state en la sesión del usuario.

url = "https://api.sistemafacturacionperu.com/conectar?" + urlencode({
    "response_type": "code",
    "client_id": os.environ["SFP_APP_CLIENT_ID"],
    "redirect_uri": "https://tusistema.com/sfp/vuelta",
    "state": state,
    "code_challenge": code_challenge,
    "code_challenge_method": "S256",
    "scope": "comprobantes.emitir comprobantes.leer consultas",
})
  • Si el client_id o la redirect_uri no están registrados, mostramos el error en la pantalla y nunca redirigimos.
  • Con la dirección ya válida, los errores vuelven a tu redirect_uri: ?error=invalid_request|invalid_scope|access_denied…&error_description=…&state=…&iss=https://api.sistemafacturacionperu.com.
  • Si todo va bien: ?code=sfp_oac_…&state=…&iss=https://api.sistemafacturacionperu.com. Comprueba que state es el tuyo antes de canjear.

3. Canjea el código (desde tu servidor)

El código vale 5 minutos y un solo uso: si se canjea dos veces, revocamos todo lo emitido con él.

curl -s https://api.sistemafacturacionperu.com/oauth/token \
  -u "$SFP_APP_CLIENT_ID:$SFP_APP_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code=sfp_oac_… \
  -d redirect_uri=https://tusistema.com/sfp/vuelta \
  -d code_verifier=EL_QUE_GUARDASTE
<?php
if (!hash_equals($_SESSION['sfp']['state'] ?? '', $_GET['state'] ?? '')) {
    http_response_code(400);
    exit('state no coincide');
}
$ch = curl_init('https://api.sistemafacturacionperu.com/oauth/token');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERPWD => getenv('SFP_APP_CLIENT_ID') . ':' . getenv('SFP_APP_CLIENT_SECRET'),
    CURLOPT_POSTFIELDS => http_build_query([
        'grant_type' => 'authorization_code',
        'code' => $_GET['code'],
        'redirect_uri' => 'https://tusistema.com/sfp/vuelta',
        'code_verifier' => $_SESSION['sfp']['verifier'],
    ]),
]);
$tokens = json_decode(curl_exec($ch), true);
// Guarda $tokens['refresh_token'], $tokens['empresa']['api_url'] y $tokens['conexion_id'] para esa empresa.
const basic = Buffer.from(`${process.env.SFP_APP_CLIENT_ID}:${process.env.SFP_APP_CLIENT_SECRET}`).toString('base64');
const res = await fetch('https://api.sistemafacturacionperu.com/oauth/token', {
  method: 'POST',
  headers: { Authorization: `Basic ${basic}`, 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: req.query.code,
    redirect_uri: 'https://tusistema.com/sfp/vuelta',
    code_verifier: req.session.sfp.verifier,
  }),
});
const tokens = await res.json();
// Guarda tokens.refresh_token, tokens.empresa.api_url y tokens.conexion_id para esa empresa.
r = requests.post(
    "https://api.sistemafacturacionperu.com/oauth/token",
    auth=(os.environ["SFP_APP_CLIENT_ID"], os.environ["SFP_APP_CLIENT_SECRET"]),
    data={
        "grant_type": "authorization_code",
        "code": request.args["code"],
        "redirect_uri": "https://tusistema.com/sfp/vuelta",
        "code_verifier": session["sfp"]["verifier"],
    },
    timeout=30,
)
tokens = r.json()
{
  "access_token": "sfp_oat_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "sfp_ort_…",
  "scope": "comprobantes.emitir comprobantes.leer consultas",
  "conexion_id": 42,
  "empresa": {
    "ruc": "20601030013",
    "razon_social": "EMPRESA CONECTADA S.A.C.",
    "api_url": "https://miempresa.sistemafacturacionperu.com/api/v1"
  }
}

Las credenciales van por Authorization: Basic o como client_id y client_secret en el cuerpo (no las dos). Guarda empresa.api_url: el token solo vale en la API de esa empresa.

curl -s https://miempresa.sistemafacturacionperu.com/api/v1/yo -H "Authorization: Bearer sfp_oat_…"

4. Renueva y revoca

El token de acceso dura 1 hora. Renuévalo con el de renovación: cada uso te da uno nuevo y el anterior deja de valer. Si alguien reusa uno ya usado, revocamos toda la cadena y la empresa tiene que volver a pulsar «Permitir»: guarda siempre el último.

curl -s https://api.sistemafacturacionperu.com/oauth/token -u "$SFP_APP_CLIENT_ID:$SFP_APP_CLIENT_SECRET" \
  -d grant_type=refresh_token -d refresh_token=sfp_ort_…
  • POST /oauth/revoke (RFC 7009) con token= (y token_type_hint si quieres): un token de acceso deja de servir; un token de renovación desconecta la empresa (úsalo cuando el usuario quite la conexión en tu sistema). Siempre responde 200.
  • Límites: 30 llamadas por minuto por IP y 120 por client_id (429 temporarily_unavailable con Retry-After).

5. Cuando la empresa desconecta

En Configuración › Sistema › Integraciones › Aplicaciones conectadas la empresa ve tu aplicación, quién la autorizó, cuándo, sus permisos, su local y el último uso, y puede Desconectar. Al instante tus tokens responden 401 conexion_revocada y, si registraste un webhook, te llega conexion.revocada firmado como los webhooks:

{
  "id": "7b0e3c8a-5d1f-4f0e-9a51-2a8c7c9e1f30",
  "evento": "conexion.revocada",
  "creado": "2026-12-26T10:15:00-05:00",
  "data": {
    "conexion_id": 42,
    "empresa": { "ruc": "20601030013", "razon_social": "EMPRESA CONECTADA S.A.C." },
    "motivo": "empresa",
    "revocada": "2026-12-26T10:15:00-05:00"
  }
}

La conexión es de la empresa: si quien autorizó cambia su contraseña o deja la empresa, sigue funcionando.

Quién puede dar permiso

El administrador (titular) de la empresa y quien tenga la función «Gestionar tokens de API». Un vendedor o un cajero sin esa función ve «Tu usuario no puede conectar sistemas». Ni nuestro soporte ni el portal del contador pueden autorizar en nombre de la empresa. Con la cuenta suspendida o el pago vencido, la empresa no puede conectar y la API responde 423 cuenta_bloqueada.

Plan Básico frente a PRO

Básico PRO y Empresa
Conectar Sí, solo para emitir Sí, con todos los permisos
Qué puede hacer tu sistema Emitir facturas, boletas, notas y guías (comprobantes.emitir, guias.emitir); ver su estado y bajar PDF, XML y CDR (comprobantes.leer, guias.leer); anular (comprobantes.anular); consultar RUC y DNI (consultas) Además: clientes, productos, stock, cotizaciones, notas de venta, retenciones, percepciones, liquidaciones, cobros y webhooks
Comprobantes al mes Los 300 del Básico, contados como en la web (422 limite_plan al pasarte) Sin límite
Claves de API propias y modo pruebas No (403 plan_sin_api) Sí
  • Puedes pedir todos los permisos: en la pantalla «Permitir», un Básico ve tachados los que su plan no cubre, con «Incluido desde PRO», y el scope de la respuesta trae solo los que sirven hoy.
  • Se revisa en cada llamada con el plan de ese momento: si la empresa baja a Básico, lo que no es de emitir responde 403 plan_insuficiente (errors.plan: ["PRO"]); si sube a PRO, funciona sin volver a conectar. Para pedir permisos nuevos, el usuario vuelve a pasar por «Permitir» (se actualiza la misma conexión).

Para desarrollar, registra también una redirect_uri de http://localhost. Recuerda que la conexión es siempre con una cuenta real: lo que emitas con ese token sigue la configuración de esa empresa (si está en el entorno «Demo», va a la beta de SUNAT; si está en «Producción», es válido ante SUNAT).

Los errores del servidor de autorización están en el catálogo de errores.

Registra tu aplicación

Mándanos el nombre y el logo de tu sistema, tu web, un correo de soporte, tus direcciones de retorno y los permisos que vas a pedir. Te respondemos con tu client_id y tu client_secret.

Portal para desarrolladores · ¿Algo no está claro o te falta un ejemplo? Escríbenos y lo mejoramos.