DEV Community

Cover image for Stripe: 10 años diseñando APIs de pagos y sus lecciones técnicas
lu1tr0n
lu1tr0n

Posted on • Originally published at elsolitario.org

Stripe: 10 años diseñando APIs de pagos y sus lecciones técnicas

Introducción

En diciembre de 2020, Michelle Bu publicó en el blog de ingeniería de Stripe una retrospectiva que se volvió lectura obligatoria para cualquiera que diseñe sistemas de cobro: diez años construyendo APIs de pagos, desde los siete endpoints del primer lanzamiento hasta la plataforma actual que mueve cientos de miles de millones de dólares al año. El texto, más que un homenaje, es un manual de cicatrices: cada decisión de diseño representa un problema que explotó en producción y obligó a repensar la forma en que una API debe representar dinero.

Para los desarrolladores en Latinoamérica que hoy construyen checkouts, marketplaces o agregadores de medios de pago locales —Mercado Pago, Kushki, dLocal, Openpay, Conekta, Wompi—, las lecciones de Stripe no son un capricho estético: son el mapa de los charcos donde ya pisaron otros. Este artículo repasa las decisiones clave y las traduce al contexto regional, con ejemplos en código que podés adaptar a cualquier pasarela.
Los objetos centrales evolucionaron de Charge a PaymentIntent en una década.

Qué pasó: del objeto Charge al PaymentIntent

Cuando Stripe abrió su API pública en septiembre de 2011, el modelo era casi ingenuo en su simplicidad: un solo objeto llamado Charge representaba el intento de cobrar una tarjeta, el resultado y el recibo. Hacer un POST con la tarjeta y el monto devolvía un Charge exitoso o fallido. Durante años, ese modelo fue suficiente: tarjeta, monto, moneda, confirmación. La web occidental todavía funcionaba casi toda con Visa, Mastercard y un puñado de procesadores estadounidenses.

El problema comenzó cuando Europa impuso PSD2 y la Autenticación Reforzada de Clientes (SCA), que obligaba a desafíos adicionales como 3D Secure en transacciones mayores a 30 euros. El objeto Charge, diseñado como una operación sincrónica, no tenía manera limpia de representar estados intermedios: ¿qué pasa cuando el banco dice "necesito que el cliente confirme con una app"? Stripe respondió en 2019 con PaymentIntent: un objeto que modela el flujo del pago, no solo el resultado, con estados explícitos como requires_action, processing y succeeded.

La lección es más profunda de lo que parece: una API de pagos moderna no puede ser sincrónica. El pago de 2011 era una transacción atómica; el pago de 2026 es una conversación con múltiples actores (banco emisor, red, procesador, regulador, usuario) donde cada paso puede requerir tiempo, consentimiento adicional o retries.

Contexto e historia: por qué Stripe reescribió su modelo

Para entender la magnitud del cambio hay que recordar el contexto. Stripe fue fundada en 2010 por los hermanos Collison cuando la mayoría de pasarelas te obligaban a leer PDFs de 400 páginas, firmar contratos físicos y esperar semanas. Su propuesta —siete líneas de código para cobrar una tarjeta— era tan radical que durante años la diferencia entre integrar Stripe e integrar a un competidor se medía en días de trabajo versus meses.

Ese developer experience se sostuvo sobre una premisa: la API es el producto. No los comerciales, no el dashboard, no los acuerdos con bancos. Eso obligó a Stripe a tomarse el versionado con una seriedad que pocas empresas respetan. Cada cliente recibe una versión fija de la API (datada, tipo 2024-06-20), y Stripe mantiene compatibilidad hacia atrás indefinidamente. Una integración escrita en 2013 sigue funcionando hoy sin cambios, porque los breaking changes se aíslan por versión y el cliente decide cuándo actualizar.

💭 Clave: el versionado por fecha en headers, en lugar de /v1/, /v2/ en la URL, permite evolucionar la API sin fragmentar a los clientes ni obligar a migraciones masivas. Es la diferencia entre un museo de APIs deprecated y una plataforma viva.

Datos y cifras: la escala del experimento

Los números que Stripe ha publicado en sus reportes anuales y eventos de desarrolladores permiten dimensionar la magnitud del desafío:

  • 10+ años de evolución continua sin romper clientes.- Cientos de versiones de API convivientes en producción simultáneamente.- Más de 135 monedas soportadas, cada una con reglas de redondeo y límites distintos.- Más de 50 métodos de pago: tarjetas, SEPA, ACH, Boleto, OXXO, Alipay, WeChat Pay, Klarna, BNPL.- Webhooks con hasta 3 días de reintentos exponenciales ante fallas del receptor.- Idempotency keys válidas por 24 horas, garantizando que un POST duplicado por timeout no cobre dos veces.

Para comparar: Mercado Pago procesa alrededor de 151 mil millones de dólares anuales (TPV 2024), dLocal reportó 7.2 mil millones en 2024, y Kushki atiende a más de 10 países en la región. Las decisiones de diseño que Stripe pulió en diez años hoy son el piso mínimo esperable para cualquier agregador serio.

Idempotencia: la red salvavidas

De todas las decisiones de diseño, la idempotencia es probablemente la más copiada —y la más mal implementada— en la región. La idea es simple: cada request mutante lleva un header Idempotency-Key con un UUID generado por el cliente; si el servidor ya procesó ese key, devuelve la respuesta cacheada en lugar de ejecutar de nuevo.

curl -X POST https://api.stripe.com/v1/payment_intents \
  -u sk_test_xxx: \
  -H "Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -d amount=2000 \
  -d currency=usd \
  -d payment_method_types[]=card
Enter fullscreen mode Exit fullscreen mode

La implementación ingenua del backend falla feo: si solo guardás el key y devolvés 409 en duplicados, rompés el contrato cuando el cliente reintenta por un timeout real. La implementación correcta guarda tanto el key como la respuesta completa (cuerpo + status code) con un TTL. Si el request idéntico llega de nuevo dentro de ese TTL, se devuelve la misma respuesta byte-por-byte.

# Pseudocódigo en Python para servidor con Redis
import hashlib, json, redis

r = redis.Redis()

def handle_charge(idempotency_key: str, payload: dict):
    cached = r.get(f"idem:{idempotency_key}")
    if cached:
        return json.loads(cached)

    result = process_payment(payload)
    r.setex(
        f"idem:{idempotency_key}",
        86400,  # 24h
        json.dumps(result),
    )
    return result
Enter fullscreen mode Exit fullscreen mode

⚠️ Ojo: si dos requests con el mismo Idempotency-Key llegan con cuerpos distintos, Stripe devuelve un error explícito. No "ganás" el último: rechazás el conflicto. Esta decisión pequeña evita bugs donde un retry con datos corruptos sobrescribe la operación original.

Impacto y análisis: qué copiar y qué no

La lectura cuidadosa del texto de Michelle Bu deja al menos seis principios que sobreviven a modas y frameworks:

1. Modelá flujos, no eventos

Un pago no es una acción única, es una máquina de estados. Cuando tu recurso central es un objeto con transiciones explícitas (requires_payment_method, requires_confirmation, requires_action, processing, succeeded, canceled), tu cliente puede razonar sobre el sistema sin adivinar. Cuando el recurso es "un evento que ocurrió", cada nueva regulación o método de pago fuerza un nuevo tipo de evento y fragmenta la API.

2. Expandir antes que proliferar

Stripe evitó durante años crear endpoints específicos por método de pago. En lugar de /charges/oxxo, /charges/boleto, /charges/pix, mantuvo un único /payment_intents parametrizado con payment_method_types. Esto mantiene la superficie de la API pequeña y obliga a que los nuevos métodos respeten el modelo común.

3. Webhooks son contratos, no conveniencias

Los webhooks de Stripe están firmados, versionados, reintentados con backoff exponencial y entregados con garantía at-least-once. Eso obliga al receptor a ser idempotente también. En LATAM es habitual ver integraciones donde el webhook se dispara una sola vez y si falla, se pierde: ese diseño es incompatible con cualquier sistema financiero serio.

4. Expandable fields

Stripe introdujo el parámetro expand para que el cliente pidiera explícitamente subobjetos embebidos en la respuesta. Así evitó el clásico dilema entre endpoints chicos (N+1 queries desde el cliente) y endpoints obesos (payload inflado). El cliente elige.

GET /v1/charges/ch_1234?expand[]=customer&expand[]=balance_transaction
Enter fullscreen mode Exit fullscreen mode

5. Errores tipados y accionables

Un error de Stripe nunca es solo un código HTTP: trae type, code, decline_code, param y un message legible. El cliente puede programar reacciones específicas (pedir otra tarjeta, reintentar en 10 minutos, mostrar 3DS) sin parsear strings.

6. El dashboard es un cliente más

El dashboard de Stripe consume la misma API pública. Eso garantiza que cualquier operación que el equipo interno hace en la UI, el cliente externo también puede hacerla programáticamente. No hay features "solo de dashboard".
El flujo moderno de PaymentIntent expone cada transición al cliente.

Diagrama del flujo moderno

stateDiagram-v2;
    [*] --> requires_payment_method;
    requires_payment_method --> requires_confirmation: attach PM;
    requires_confirmation --> requires_action: 3DS;
    requires_action --> processing: challenge ok;
    requires_confirmation --> processing: confirm;
    processing --> succeeded;
    processing --> requires_payment_method: failed;
    succeeded --> [*];
Enter fullscreen mode Exit fullscreen mode

Qué sigue: APIs de pagos en la era de los agentes

La próxima década de APIs de pagos va a tener tres vectores dominantes. El primero es pagos agénticos: asistentes de IA que inician transacciones en nombre del usuario. Esto requiere nuevos primitivos —scopes finos, consentimiento delegado, límites por sesión— que Stripe ya empezó a mover con productos como Stripe Apps y el Agent Toolkit lanzado en 2024.

El segundo vector es pagos instantáneos locales: PIX en Brasil, SPEI en México, Transferencias 3.0 en Argentina, Transfiya en Colombia. Estos sistemas erosionan el dominio de las tarjetas y obligan a las pasarelas a exponer primitivas de transferencia cuenta-a-cuenta con liquidación en segundos, no days.

El tercero es stablecoins y rieles cripto: Stripe reactivó sus capacidades cripto en 2024 con USDC, y procesadores regionales como Bitso Business ya ofrecen liquidación en stablecoins para pagos transfronterizos. La diferencia de costo con SWIFT hace que esta categoría deje de ser experimento y pase a ser infraestructura.

💡 Tip: si estás diseñando tu propia pasarela o wrapper sobre proveedores locales, copiá de Stripe la idempotencia, los webhooks firmados y el versionado por fecha. Es lo más barato de implementar al principio y lo más caro de agregar después.

📖 Resumen en Telegram: Ver resumen

Preguntas frecuentes

¿Por qué Stripe cambió de Charge a PaymentIntent?

Porque PSD2 y SCA en Europa introdujeron flujos asíncronos con autenticación del cliente que no cabían en un objeto sincrónico. PaymentIntent modela explícitamente los estados intermedios (requires_action, processing) y deja al cliente reaccionar a cada transición.

¿Qué es una idempotency key y por qué importa?

Es un identificador único que el cliente envía en cada request mutante. El servidor guarda la respuesta asociada a ese key por un TTL (24h en Stripe), y si llega un request duplicado devuelve la misma respuesta sin reprocesar. Evita cobros duplicados por timeouts o retries accidentales.

¿Cómo versiona Stripe su API sin romper clientes?

Usa versionado por fecha en el header Stripe-Version. Cada cuenta tiene una versión fija asignada al momento de registrarse, y Stripe mantiene compatibilidad hacia atrás para todas las versiones publicadas. Actualizar es una decisión explícita del cliente, no impuesta.

¿Qué lecciones se pueden aplicar a pasarelas en LATAM?

Cuatro mínimos: idempotencia obligatoria en operaciones mutantes, webhooks firmados con reintentos exponenciales, versionado por fecha en lugar de v1/v2 en la URL, y un modelo de recurso central con estados explícitos en lugar de eventos aislados.

¿PaymentIntent reemplaza completamente a Charge?

No. Charge sigue existiendo como objeto subyacente: un PaymentIntent exitoso genera uno o más Charges. Stripe mantiene ambos para no romper integraciones antiguas, pero las nuevas integraciones deben construirse sobre PaymentIntent.

¿Cómo verifico webhooks de Stripe correctamente?

Con el header Stripe-Signature y tu webhook secret. Stripe firma cada payload con HMAC-SHA256 sobre el timestamp + cuerpo. Rechazá eventos con timestamp mayor a 5 minutos para prevenir replays, y procesá cada event id una sola vez.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Top comments (0)