phpguzzle.org
enrudees
Stripe – Docs

SDK de Stripe para PHP: referencia del API con ejemplos

El SDK de Stripe para PHP (stripe/stripe-php) envuelve cada endpoint REST detrás de clases tipadas, maneja la autenticación, reintenta errores transitorios y deserializa respuestas en objetos a través de los cuales puedes navegar con propiedades. Pero la documentación oficial muestra cada método aislado, sin el contexto que las une: cuándo expandir un objeto anidado vs. buscarlo por separado, cómo se comporta la paginación en datasets grandes, qué es el objeto de respuesta por dentro. Eso es lo que cubre esta referencia.

Si aún no has instalado el SDK, la guía de integración de pagos cubre la configuración con Composer, las claves API y tu primer PaymentIntent. Cada ejemplo de código PHP con Stripe en esta página es funcional y listo para copiar.

StripeClient vs Stripe::setApiKey

El SDK tiene dos convenciones de llamada, y mezclarlas es la fuente de una cantidad sorprendente de bugs.

Estático global (legacy):

\Stripe\Stripe::setApiKey('sk_test_...');

$customer = \Stripe\Customer::create(['email' => '[email protected]']);
$intent   = \Stripe\PaymentIntent::retrieve('pi_abc123');

Basado en instancia (recomendado desde v7.33):

$stripe = new \Stripe\StripeClient('sk_test_...');

$customer = $stripe->customers->create(['email' => '[email protected]']);
$intent   = $stripe->paymentIntents->retrieve('pi_abc123');

La diferencia no es solo cosmética. setApiKey almacena la clave en una propiedad estática. Cualquier código que corra en el mismo proceso comparte la misma clave. Eso incluye suites de test, queue workers procesando jobs para diferentes cuentas Stripe, y cualquier otra cosa que viva en la misma memoria. Un job sobrescribe la clave, el siguiente envía una petición con credenciales equivocadas, y obtienes “No such payment_intent” sin indicación de que la clave fue el problema.

StripeClient mantiene la clave en la instancia. Dos instancias con claves diferentes pueden coexistir en el mismo proceso. En un queue worker de Laravel manejando cuentas Connect, esa diferencia es lo único entre tú y una fuga de datos entre cuentas.

// Connect: operando en nombre de una cuenta conectada
$stripe = new \Stripe\StripeClient([
    'api_key'        => 'sk_test_platform_key',
    'stripe_account' => 'acct_connected_123',
]);

Para código nuevo, siempre usa StripeClient. El patrón estático sigue funcionando y Stripe no ha anunciado fecha de remoción, pero los endpoints nuevos solo vienen con métodos basados en servicios del cliente. La guía de Stripe en Laravel cubre el setup completo del Service Provider con singleton.

Autenticación y claves API

Stripe te da dos pares de claves, uno para test y otro para live:

PrefijoPropósitoDónde va
sk_test_Clave secreta, modo testSolo servidor, variable de entorno
sk_live_Clave secreta, modo liveSolo servidor, variable de entorno
pk_test_Clave publicable, modo testNavegador (Stripe.js)
pk_live_Clave publicable, modo liveNavegador (Stripe.js)

Las claves secretas autentican cada llamada al API del lado del servidor. Las publicables son seguras de exponer en JavaScript del cliente; solo pueden confirmar pagos y crear tokens, no leer ni escribir datos de la cuenta.

Las claves de Stripe no expiran. Viven hasta que las rotas. Rotar genera una nueva clave con una ventana de gracia (24 horas para claves secretas, configurable) durante la cual ambas funcionan. Después de la ventana, la clave antigua deja de autenticar. Rota claves desde el Dashboard en Developers > API keys.

Las restricted keys son un tercer tipo: las creas en el Dashboard con permisos específicos (leer cobros, escribir clientes, nada más). Úsalas para microservicios e integraciones de terceros donde entregar una clave secreta completa es demasiado amplio.

Claves fuera del control de versiones

La regla: claves en variables de entorno, nunca en archivos PHP. Una clave sk_live_ filtrada da acceso completo a tu cuenta de Stripe.

// Bien: leer desde env al arrancar
$stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET_KEY'));

// Mal: hardcodeado, terminará en git
$stripe = new \Stripe\StripeClient('sk_live_claveRealAquí');

Llamadas al API: el patrón de métodos

Cada recurso sigue la misma forma. Los métodos mapean a verbos HTTP:

Método SDKHTTPEjemplo
->create([...])POSTCrear un Customer
->retrieve('id')GETObtener un PaymentIntent
->update('id', [...])POSTActualizar una Subscription
->delete('id')DELETEEliminar un cupón
->all([...])GETListar cobros

El primer argumento de retrieve, update y delete es siempre el ID del objeto como string. Create y list toman un array asociativo de parámetros. Esta firma es consistente en todos los recursos:

$stripe = new \Stripe\StripeClient('sk_test_...');

// Create
$customer = $stripe->customers->create([
    'email' => '[email protected]',
    'name'  => 'María López',
    'metadata' => ['internal_id' => '42'],
]);

// Retrieve
$customer = $stripe->customers->retrieve('cus_abc123');

// Update
$stripe->customers->update('cus_abc123', [
    'name' => 'María López García',
]);

// Delete
$stripe->customers->delete('cus_abc123');

// List
$customers = $stripe->customers->all(['limit' => 10]);

Parámetros extra junto al ID

retrieve toma el ID y un array de parámetros opcional. Parámetros del API como expand van en ese array:

$intent = $stripe->paymentIntents->retrieve(
    'pi_abc123',
    ['expand' => ['customer', 'payment_method']]
);

update toma el ID, un array de parámetros para los campos a cambiar, y un tercer array opcional para opciones a nivel de petición (clave de idempotencia, header Stripe-Account). expand es un parámetro regular del API, va en el array de parámetros junto con tus datos:

$stripe->paymentIntents->update('pi_abc123', [
    'metadata' => ['order_id' => '99'],
    'expand'   => ['customer'],
]);

// Opciones de petición van en el tercer argumento
$stripe->paymentIntents->update(
    'pi_abc123',
    ['metadata' => ['order_id' => '99']],
    ['idempotency_key' => 'update_pi_abc123_v2']
);

Expandir objetos anidados

Por defecto, el API devuelve objetos relacionados como IDs desnudos. El campo customer de un PaymentIntent viene como "cus_abc123", no el objeto Customer completo. Expand le dice a Stripe que incluya el objeto completo en la respuesta para que te ahorres la segunda llamada.

$intent = $stripe->paymentIntents->retrieve('pi_abc123', [
    'expand' => ['customer', 'payment_method'],
]);

// Ahora estos son objetos completos, no strings
echo $intent->customer->email;
echo $intent->payment_method->card->last4;

Profundidad máxima

Cuatro niveles de anidamiento, y no más de 10 expansiones por petición. Rutas separadas por punto recorren la cadena:

$charge = $stripe->charges->retrieve('ch_abc', [
    'expand' => ['payment_intent.customer.default_source'],
]);

Eso expande el PaymentIntent dentro del Charge, el Customer dentro del PaymentIntent, y el default source del Customer. Ir más profundo de cuatro niveles o pedir más de 10 expansiones devuelve un error.

Expand en llamadas de lista

Al listar objetos, agrega el prefijo data. a los campos expandidos:

$charges = $stripe->charges->all([
    'limit' => 50,
    'expand' => ['data.customer', 'data.balance_transaction'],
]);

foreach ($charges->data as $charge) {
    echo $charge->customer->email;
    echo $charge->balance_transaction->fee;
}

Nota de rendimiento: expandir múltiples campos en una llamada de lista con un limit alto hace la respuesta significativamente más grande y lenta. Stripe obtiene cada objeto expandido internamente.

Expand en create

Expand funciona en create también. Útil cuando necesitas el objeto completo que se auto-genera:

$session = $stripe->checkout->sessions->create([
    'mode' => 'payment',
    'line_items' => [['price' => 'price_abc', 'quantity' => 1]],
    'success_url' => 'https://example.com/thanks',
    'expand' => ['payment_intent'],
]);

// payment_intent es el objeto completo, no solo pi_xxx
echo $session->payment_intent->status;

Paginación

Los endpoints de lista devuelven como máximo 100 objetos por llamada (10 por defecto). Stripe usa paginación basada en cursor: pasas el ID del último objeto que recibiste, y el API devuelve la siguiente página empezando después de ese cursor.

Paginación manual

$hasMore = true;
$startingAfter = null;

while ($hasMore) {
    $params = ['limit' => 100];
    if (null !== $startingAfter) {
        $params['starting_after'] = $startingAfter;
    }

    $charges = $stripe->charges->all($params);
    
    foreach ($charges->data as $charge) {
        processCharge($charge);
    }

    $hasMore = $charges->has_more;
    if ($charges->data) {
        $startingAfter = end($charges->data)->id;
    }
}

Auto-paginación

El SDK provee autoPagingIterator() que maneja la lógica de cursor internamente:

$charges = $stripe->charges->all(['limit' => 100]);

foreach ($charges->autoPagingIterator() as $charge) {
    // Itera por TODOS los charges, obteniendo páginas nuevas automáticamente
    processCharge($charge);
}

Internamente, autoPagingIterator hace una nueva llamada al API cada vez que la página actual se agota. El parámetro limit controla el tamaño de página, no el total. Con limit => 100, cada petición HTTP trae 100 objetos; el iterador sigue hasta que has_more sea false.

No hay una forma integrada de detener después de N objetos totales. Si solo necesitas los primeros 500 charges, agrega un contador:

$count = 0;
foreach ($charges->autoPagingIterator() as $charge) {
    processCharge($charge);
    if (++$count >= 500) break;
}

Filtrar listas

La mayoría de endpoints de lista aceptan filtros que acotan resultados del lado del servidor. Filtrar antes de paginar siempre es preferible a traer todo y filtrar en PHP:

// Charges de los últimos 7 días
$charges = $stripe->charges->all([
    'limit'   => 100,
    'created' => ['gte' => strtotime('-7 days')],
]);

// Suscripciones de un cliente específico
$subs = $stripe->subscriptions->all([
    'customer' => 'cus_abc123',
    'status'   => 'active',
    'limit'    => 100,
]);

El filtro created acepta gt, gte, lt, lte como claves con timestamps Unix.

Objetos de respuesta

Cada respuesta del API llega como un StripeObject (o una subclase como Customer, PaymentIntent, etc.). Estos objetos se comportan como un híbrido: puedes acceder a campos como propiedades ($customer->email) o como claves de array ($customer['email']), pero no son ni objetos planos ni arrays planos.

Convertir a JSON

La trampa más común: json_encode($stripeObject) funciona, pero serializa solo los datos públicos. Si necesitas la respuesta cruda del API como string JSON:

$customer = $stripe->customers->retrieve('cus_abc123');

// Funciona: serializa los campos de datos del objeto
$json = json_encode($customer);

// También funciona: toArray() primero, luego encode
$array = $customer->toArray();
$json  = json_encode($array, JSON_PRETTY_PRINT);

toArray() es la opción más segura para persistencia. Devuelve un array asociativo plano sin internals del SDK. Si almacenas respuestas de Stripe en una columna de base de datos o cache, usa toArray() al recuperar y json_encode sobre el resultado.

Acceso a campos null

Stripe devuelve null para campos que no se establecieron. El SDK preserva ese null, así que $customer->phone es null y no una propiedad indefinida. Usa el operador de coalescencia nula:

$phone = $customer->phone ?? 'sin teléfono registrado';

No es necesario isset() salvo que distingas “el campo no se devolvió” de “el campo se devolvió como null”, lo cual rara vez importa con la API de Stripe.

La propiedad lastResponse

Cada StripeObject lleva lastResponse, que contiene la respuesta HTTP cruda de la llamada al API que lo produjo:

$customer = $stripe->customers->retrieve('cus_abc123');

$httpStatus  = $customer->lastResponse->code;
$requestId   = $customer->lastResponse->headers['Request-Id'];
$rawBody     = $customer->lastResponse->body;

Request-Id es esencial para tickets de soporte de Stripe. Regístralo junto con cada llamada al API en producción e inclúyelo al abrir un ticket de soporte.

Tarjetas de prueba y tokens de test

El modo test es un sandbox completo. No se mueve dinero, no se contacta a las redes de tarjetas. Pero los objetos en test mode son objetos reales del API con IDs reales, y cada funcionalidad opera igual que en live, incluyendo webhooks, 3DS, disputas y reembolsos.

Números de tarjeta para Stripe.js / Elements

NúmeroMarcaComportamiento
4242 4242 4242 4242VisaÉxito
5555 5555 5555 4444MastercardÉxito
3782 822463 10005AmexÉxito
4000 0025 0000 3155VisaRequiere autenticación 3DS
4000 0000 0000 9995VisaRechazada: insufficient_funds
4000 0000 0000 0002VisaRechazada: generic_decline
4000 0000 0000 0069VisaRechazada: expired_card
4000 0000 0000 0127VisaRechazada: incorrect_cvc

Usa cualquier fecha futura para expiración y cualquier 3 dígitos para CVC (4 para Amex).

Tokens PaymentMethod para pruebas del lado del servidor

Al probar lógica del servidor sin frontend, adjunta PaymentMethods de prueba pre-construidos:

$stripe = new \Stripe\StripeClient('sk_test_...');

$intent = $stripe->paymentIntents->create([
    'amount'         => 2000,
    'currency'       => 'usd',
    'payment_method' => 'pm_card_visa',
    'confirm'        => true,
    'automatic_payment_methods' => [
        'enabled'         => true,
        'allow_redirects' => 'never',
    ],
]);

echo $intent->status; // "succeeded"

Los tokens pm_card_* omiten el paso de Stripe.js por completo. Útiles para tests de integración, pruebas del pipeline de webhooks y CI.

TokenSimula
pm_card_visaPago Visa exitoso
pm_card_mastercardMastercard exitoso
pm_card_chargeDeclinedgeneric_decline
pm_card_chargeDeclinedInsufficientFundsinsufficient_funds
pm_card_authenticationRequiredRequiere 3DS

El formato legacy tok_visa / tok_chargeDeclined sigue funcionando a través de una conversión interna, pero pm_card_* es la forma recomendada actual. Para la referencia completa de códigos de rechazo, consulta la guía de errores.

Versionado del API

El API de Stripe evoluciona a través de versiones con fecha como 2026-03-25.dahlia. Cada versión major puede cambiar la forma de las respuestas, renombrar campos o alterar comportamientos por defecto.

Cómo el SDK fija la versión

El SDK fija su versión del API a la que era actual cuando se construyó ese release del SDK. Tu cuenta en el Dashboard tiene una versión por defecto separada. Las peticiones desde el SDK usan la versión del SDK, no la del Dashboard.

echo \Stripe\Stripe::getApiVersion();
// ej., "2026-03-25.dahlia"

Sobrescribir la versión

Puedes forzar una versión específica por cliente o por petición:

// Por cliente
$stripe = new \Stripe\StripeClient([
    'api_key'        => 'sk_test_...',
    'stripe_version' => '2024-12-18.acacia',
]);

Sobreescribe con moderación. Ejecutar una llamada al API con una versión diferente al resto de tu código significa que las formas de respuesta pueden diferir en tu codebase, lo cual lleva a bugs sutiles.

Webhooks y versiones

Los eventos de webhook se entregan en la versión del API del endpoint que los creó, no en la versión que usa tu SDK. Si tu endpoint de webhook está configurado en el Dashboard con versión X, pero tu SDK espera versión Y, la estructura del evento puede no coincidir con lo que tu código espera. Mantenlos alineados. La guía de webhooks cubre esto en el contexto de verificación de firma.

Idempotencia

Stripe admite claves de idempotencia en todas las peticiones POST. Pasa una clave única, y si la misma petición se dispara dos veces (reintento de red, doble clic del usuario, re-entrega de webhook), Stripe devuelve el mismo resultado sin crear un objeto duplicado.

$stripe = new \Stripe\StripeClient('sk_test_...');

$intent = $stripe->paymentIntents->create([
    'amount'   => 5000,
    'currency' => 'usd',
], [
    'idempotency_key' => 'order_42_payment_attempt_1',
]);

La clave está acotada a la petición. Stripe almacena el resultado durante 24 horas; una segunda petición con la misma clave y los mismos parámetros devuelve la respuesta cacheada. Una segunda petición con la misma clave pero parámetros diferentes devuelve un error 400.

Qué hace una buena clave: tu identificador de dominio más la intención. order_{id}_payment_{attempt} funciona. Un UUID también funciona pero es más difícil de depurar. Evita timestamps solos; dos peticiones en el mismo milisegundo con el mismo timestamp crean el problema que la idempotencia debería prevenir.

Dónde importa más usarla

  • Creación de PaymentIntent. Un timeout de red después de que Stripe procesó la petición pero antes de que tu servidor recibiera la respuesta. Sin clave, reintentas y creas un segundo cobro.
  • Reembolsos. Mismo escenario. Un doble reembolso es peor que un doble cargo porque el cliente no se queja y tú no lo ves hasta la reconciliación.
  • Webhooks. Tu handler debería ser idempotente por diseño (verificar si la orden ya está marcada como pagada antes de marcarla otra vez). La clave de idempotencia protege el lado saliente; la idempotencia del handler protege el lado entrante.

Jerarquía de excepciones

El SDK lanza excepciones tipadas para cada error del API:

\Stripe\Exception\ApiErrorException (base abstracta)
├── CardException              // 402 - tarjeta rechazada, fondos insuficientes
├── InvalidRequestException    // 400 - parámetros faltantes, ID incorrecto
├── AuthenticationException    // 401 - clave API inválida
├── ApiConnectionException     // fallo de red, timeout, DNS
├── IdempotencyException       // clave de idempotencia conflictiva
├── PermissionException        // 403 - restricted key sin permisos
├── RateLimitException         // 429 - demasiadas peticiones
└── UnknownApiErrorException   // cualquier otra cosa del API

SignatureVerificationException  // firma de webhook incorrecta (NO extiende ApiErrorException)

SignatureVerificationException no extiende ApiErrorException porque viene de la verificación local de firma, no de una llamada al API. Capturar ApiErrorException en un handler general no la atrapará.

El orden importa. CardException extiende ApiErrorException, así que si ApiErrorException aparece primero en tu cadena de catch, el bloque de CardException nunca se ejecuta:

try {
    $intent = $stripe->paymentIntents->create([...]);
} catch (\Stripe\Exception\CardException $e) {
    // Debe ir ANTES que ApiErrorException
    $decline = $e->getError()->decline_code;
    error_log("Tarjeta rechazada: {$decline}");
} catch (\Stripe\Exception\RateLimitException $e) {
    // 429: backoff y reintentar
    sleep(2);
} catch (\Stripe\Exception\InvalidRequestException $e) {
    error_log("Petición inválida: " . $e->getMessage());
} catch (\Stripe\Exception\ApiErrorException $e) {
    error_log("Error de Stripe: " . $e->getMessage());
}

El desglose completo de tipos de error, códigos de rechazo y estrategias de recuperación está en la guía de errores.

Metadata

Cada objeto principal de Stripe acepta un hash metadata: hasta 50 claves, cada clave hasta 40 caracteres, cada valor hasta 500 caracteres. Metadata es tu puente entre el mundo de Stripe y el tuyo.

$intent = $stripe->paymentIntents->create([
    'amount'   => 7500,
    'currency' => 'eur',
    'metadata' => [
        'order_id'    => '1042',
        'campaign'    => 'summer_sale',
        'internal_id' => 'usr_887',
    ],
]);

Metadata viaja con el objeto a través de su ciclo de vida. Cuando el PaymentIntent tiene éxito y dispara un evento de webhook, $event->data->object->metadata->order_id te da "1042". Así es como el handler del webhook sabe qué orden cumplir sin consultar tu base de datos por monto o timestamp.

Metadata no se propaga entre objetos

Metadata en una Checkout Session no se copia automáticamente al PaymentIntent que crea. Si tu handler de webhook escucha payment_intent.succeeded y lee metadata ahí, estará vacío salvo que explícitamente configures payment_intent_data.metadata al crear la Session:

$session = $stripe->checkout->sessions->create([
    'mode' => 'payment',
    'line_items' => [['price' => 'price_abc', 'quantity' => 1]],
    'success_url' => 'https://example.com/thanks',
    'metadata' => ['order_id' => '42'],
    'payment_intent_data' => [
        'metadata' => ['order_id' => '42'],
    ],
]);

O sáltate la duplicación y escucha checkout.session.completed en vez de payment_intent.succeeded. La guía de Checkout lo cubre en detalle.

Logging y depuración

Request-Id

Cada respuesta del API incluye un header Request-Id. Regístralo:

$customer = $stripe->customers->create(['email' => '[email protected]']);
$requestId = $customer->lastResponse->headers['Request-Id'];
error_log("Petición Stripe: {$requestId}");

Cuando algo sale mal en producción y abres un ticket con soporte de Stripe, lo primero que piden es el Request-Id.

Logging a nivel de SDK

El SDK puede registrar cada petición y respuesta HTTP. Espera un logger PSR-3 (el paquete psr/log):

\Stripe\Stripe::setLogger(new class extends \Psr\Log\AbstractLogger {
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        error_log("[Stripe {$level}] {$message}");
    }
});

En producción, configura el nivel de log a error o desactiva el logging. El logging completo de petición/respuesta incluye fingerprints de tarjetas y emails de clientes que probablemente no quieres en un archivo de log.

Patrones comunes

Verificar si un cliente existe antes de crear

$existing = $stripe->customers->search([
    'query' => "email:'[email protected]'",
]);

if ($existing->data) {
    $customer = $existing->data[0];
} else {
    $customer = $stripe->customers->create([
        'email' => '[email protected]',
    ]);
}

Nota: el Search API es eventualmente consistente. Un cliente creado hace un segundo puede no aparecer aún en los resultados de búsqueda. Para lookups en tiempo real, almacena el ID de Stripe en tu propia base de datos y recupera por ID.

Actualizar el método de pago por defecto

$stripe->customers->update('cus_abc123', [
    'invoice_settings' => [
        'default_payment_method' => 'pm_newCard456',
    ],
]);

El campo antiguo default_source sigue funcionando para integraciones legacy pero invoice_settings.default_payment_method es la ruta actual.

Obtener un cobro con desglose de fees

$charge = $stripe->charges->retrieve('ch_abc', [
    'expand' => ['balance_transaction'],
]);

$fee    = $charge->balance_transaction->fee;
$net    = $charge->balance_transaction->net;
$details = $charge->balance_transaction->fee_details;

El balance_transaction es donde vive la comisión de procesamiento de Stripe. No está disponible en el Charge ni en el PaymentIntent directamente. La guía de comisiones y pricing cubre la lógica de fees en profundidad.

Qué es el SDK de Stripe

Un SDK (Software Development Kit) en este contexto es una librería PHP que maneja la fontanería HTTP de hablar con el API REST de Stripe. En vez de construir peticiones curl manualmente, configurar headers, parsear respuestas JSON y manejar errores, el SDK te da $stripe->customers->create([...]) y devuelve un objeto tipado.

Stripe publica SDKs oficiales para PHP, Python, Ruby, Node.js, Java, Go y .NET. La documentación del SDK de PHP está en el repo oficial, y el paquete es mantenido directamente por Stripe, no por la comunidad. Las actualizaciones siguen los cambios del API en cuestión de días. El API es propietario, pero el SDK es open source bajo licencia MIT; el código fuente vive en github.com/stripe/stripe-php.

Qué sigue

¿Has visto algo inexacto en esta página?

Reportar un error