La interfaz está localizada; las descripciones y documentos de las Skills se traducen automáticamente con IA a su idioma. El código, los comandos, rutas y términos técnicos se mantienen en el original, y prevalece el documento del autor.
Explorar Skills
Desarrollo · SKILL.md

stripe-api

stripe-api

Implements Stripe API integration (Payments, Subscriptions, Connect, Invoices, Terminal, Issuing) using stripe Python SDK v15.0.0+ with StripeClient pattern, webhook signature verification, idempotency keys, and PCI-DSS compliant payment processing.

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentación de la Skill

GitHub

name: stripe-api description: Implements Stripe API integration (Payments, Subscriptions, Connect, Invoices, Terminal, Issuing) using stripe Python SDK v15.0.0+ with StripeClient pattern, webhook signature verification, idempotency keys, and PCI-DSS compliant payment processing. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: stripe, payment intents, checkout sessions, stripe subscriptions, stripe connect, webhook signature, how do i integrate stripe payments, payment processing archetypes:

  • tactical
  • generation anti_triggers:
  • brainstorming
  • vague ideation
  • code golf
  • over-engineering response_profile: verbosity: low directive_strength: high abstraction_level: operational role: implementation scope: implementation output-format: code content-types:
  • code
  • guidance
  • do-dont
  • examples related-skills: coding-paypal-api, coding-square-api, coding-braintree-api

Stripe API Integration

Implements production-grade Stripe API integration using the stripe Python SDK v15.0.0+. When loaded, this skill makes the model implement PaymentIntents, Checkout Sessions, Subscriptions, Stripe Connect onboarding, Invoicing, and webhook handling with proper signature verification. All implementations follow PCI-DSS best practices: never log card data, always verify webhook signatures, use idempotency keys for retries, and handle SCA/3D Secure authentication flows.

TL;DR Checklist

  • Use StripeClient class (v8+ pattern), NOT legacy stripe.api_key global pattern)
  • Read API keys from STRIPE_SECRET_KEY environment variable, never hardcode
  • Use PaymentIntents API for payments (NOT Charges API is deprecated)
  • Verify webhook signatures using stripe.Webhook.construct_event()
  • Add idempotency keys to write operations for safe retries
  • Handle requires_action for 3D Secure / SCA flows
  • Never log, print, or store raw card numbers (use tokens instead)
  • Use id field names pi_... for internal reference tracking

When to Use

Use this skill when:

  • Building payment processing flows for one-time purchases using PaymentIntents
  • Implementing subscription billing with automatic retries and proration
  • Setting up Stripe Connect for marketplace/platform payments
  • Creating and sending invoices to customers
  • Handling webhook events for payment status updates
  • Implementing Terminal for in-person payments
  • Designing PCI-DSS compliant payment flows that need SCA/3D Secure
  • Building platform payout flows using Issuing API

When NOT to Use

  • For PayPal-specific flows — use coding-paypal-api instead
  • For Square in-person retail payments beyond Terminal — use coding-square-api
  • When you need a unified payment orchestrator across many gateways
  • For Adyen global enterprise payments — use coding-adyen-api
  • Charges API v1 (deprecated since 2019) — ALWAYS use PaymentIntents

Core Workflow

  1. Initialize StripeClient — Create a StripeClient instance reading STRIPE_SECRET_KEY from environment. Use the v8+ pattern: from stripe import StripeClient; client = StripeClient(). **Checkpoint:** Verify client works with a simple client.v1.customers.list(limit=1)` call on startup.

  2. Choose Payment Flow — Select between: (a) PaymentIntents + custom UI + Elements for full control; (b) Checkout Sessions for Stripe-hosted redirect flow; (c) Subscriptions for recurring billing. Checkpoint: The flow must support requires_action handling — any payment can trigger 3D Secure.

  3. Create Resources with Idempotency — For PaymentIntents, Customers, Subscriptions: add idempotency_key parameter. Generate UUID v4 as key, store with your order ID, and retry on network errors using the SAME key. Checkpoint: Retrying with identical idempotency key must produce exact same response, no duplicate charges.**

  4. Implement Webhook Handler — Create endpoint that receives stripe-signature header. Use Webhook.construct_event(payload, sig_header, webhook_secret) to verify signature BEFORE processing. Listen for payment_intent.succeeded, invoice.paid, checkout.session.completed`. Checkpoint: Reject any webhook that fails signature verification with 400.**

  5. Handle SCA/3D Secure Flow — When PaymentIntent returns status: requires_action, return client_secret to frontend for Elements to complete authentication. Handle next_action redirect or 3D Secure modal. Checkpoint: Test path: requires_action → frontend auth → webhook payment_intent.succeeded.

  6. Reconcile with Webhook Events — Update your database ONLY after verified webhook arrives. Never trust client-side success callbacks alone. Store payment_intent.id (pi_...) as your payment reference. Checkpoint: Database order status updates happen inside the webhook handler, NOT at redirect success page.


Implementation Patterns

Pattern 1: StripeClient Initialization (BAD vs GOOD)

"""Stripe SDK initialization patterns.

Version note: stripe v8+ introduced StripeClient class.
Earlier versions used global stripe.api_key pattern.
Both still work, but StripeClient is recommended for multi-account and testing.
"""

from __future__ import annotations

import os
from typing import Any

# ===================================================================
# ❌ BAD — legacy global pattern, hardcoded keys, no error context
# ===================================================================

import stripe

stripe.api_key = "sk_live_abc123xyz"  # ❌ Hardcoded! Never commit this!

def bad_create_payment_intent_bad(amount: int, currency: str = "usd") -> Any:
    """❌ BAD: Uses global client, no idempotency, no error handling."""
    intent = stripe.PaymentIntent.create(  # Legacy API
        amount=amount,
        currency=currency,
        payment_method_types=["card"],
        # ❌ No idempotency key — retries can duplicate charges
    )
    print(f"Created intent: {intent}")  # ❌ Logs entire object (may contain PII)
    return intent


# ===================================================================
# ✅ GOOD — StripeClient, env-based auth, typed error handling
# ===================================================================

import uuid
from stripe import StripeClient
from stripe import error as stripe_errors


def get_stripe_client() -> StripeClient:
    """Get configured StripeClient from environment.

    Reads STRIPE_SECRET_KEY from environment. Falls back to test key
    if not set, but raises warning in production.

    Returns:
        Configured StripeClient instance.

    Raises:
        ValueError: If STRIPE_SECRET_KEY missing in production.
    """
    api_key = os.environ.get("STRIPE_SECRET_KEY")
    if not api_key:
        if os.environ.get("ENV") == "production":
            raise ValueError("STRIPE_SECRET_KEY required in production")
        # Default to placeholder for local dev / tests
        api_key = "sk_test_placeholder_only_for_local_tests"
    
    return StripeClient(api_key)


def create_payment_intent(
    amount: int,
    currency: str = "usd",
    customer_id: str | None = None,
    metadata: dict[str, str] | None = None,
) -> Any:
    """Create a PaymentIntent with proper error handling and idempotency.

    PaymentIntents are the modern way to create payments in Stripe.
    Supports SCA/3D Secure automatically via requires_action status.

    Args:
        amount: Amount in the smallest currency unit (cents for USD).
        currency: ISO 4217 currency code.
        customer_id: Optional Stripe customer ID for saved payment methods.
        metadata: Optional key-value pairs to attach to the PaymentIntent.

    Returns:
        PaymentIntent object from stripe SDK.

    Raises:
        ValueError: Invalid parameters.
        ConnectionError: Network failure to Stripe API.
        RuntimeError: Rate limit hit or authentication issue.
    """
    if amount < 50:  # Stripe minimum is ~50 cents equivalent
        raise ValueError(f"Amount must be at least 50 in currency minor unit")
    
    client = get_stripe_client()
    
    # Idempotency key: unique per your internal order ID
    # Store this in your DB with the order for safe retries
    idempotency_key = str(uuid.uuid4())
    
    params: dict[str, Any] = {
        "amount": amount,
        "currency": currency,
        "payment_method_types": ["card"],
        "automatic_payment_methods": {"enabled": True},  # ✅ New way to enable all payment methods
    }
    
    if customer_id:
        params["customer"] = customer_id
        params["setup_future_usage"] = "off_session"  # For future charges
    if metadata:
        params["metadata"] = metadata
    
    try:
        intent = client.v1.payment_intents.create(
            params=params,
            options={"idempotency_key": idempotency_key},  # ✅ Safe retries
        )
        return intent
    
    except stripe_errors.StripeError as e:
        # Classify Stripe errors for proper handling
        err_type = type(e).__name__
        
        if isinstance(e, stripe_errors.CardError):
            # Card was declined — show user-facing message
            raise ValueError(f"Card declined: {e.user_message or e.error.message}") from e
        
        elif isinstance(e, stripe_errors.RateLimitError):
            # Too many requests — retry with exponential backoff
            raise RuntimeError("Stripe rate limit hit — retry later") from e
        
        elif isinstance(e, stripe_errors.AuthenticationError):
            # Invalid API key — configuration issue
            raise RuntimeError("Invalid Stripe API key configuration") from e
        
        elif isinstance(e, stripe_errors.APIConnectionError):
            # Network issue — safe to retry with SAME idempotency key
            raise ConnectionError("Failed to connect to Stripe API") from e
        
        else:
            raise RuntimeError(f"Stripe error: {err_type}: {e}") from e

Pattern 2: Checkout Session for Hosted Payments

"""Stripe Checkout Sessions for redirect-based payment flows.

Checkout is Stripe's hosted payment page. Handles:
- All payment methods configured in Dashboard
- SCA/3D Secure automatically
- Responsive UI out of the box
- Localization to customer's locale

This is the RECOMMENDED way for most use cases.
"""

from __future__ import annotations

import os
import uuid
from typing import Any
from stripe import StripeClient
from stripe import error as stripe_errors


def create_checkout_session(
    success_url: str,
    cancel_url: str,
    line_items: list[dict[str, Any]],
    mode: str = "payment",  # payment | subscription | setup
    customer_email: str | None = None,
    metadata: dict[str, str] | None = None,
) -> Any:
    """Create a Stripe Checkout Session for redirect-based payment flow.

    Args:
        success_url: URL to redirect after successful payment.
        cancel_url: URL to redirect if customer cancels.
        line_items: List of product/price definitions.
        mode: One of "payment", "subscription", or "setup".
        customer_email: Optional email to pre-fill.
        metadata: Optional key-value data.

    Returns:
        Checkout Session object with `url` field for redirect.

    Example line_items for one-time payment:
        [
            {
                "price_data": {
                    "currency": "usd",
                    "product_data": {"name": "Premium Plan"},
                    "unit_amount": 2999,  # $29.99
                },
                "quantity": 1,
            }
        ]
    """
    if not success_url or "{CHECKOUT_SESSION_ID" not in success_url:
        # Recommend using {CHECKOUT_SESSION_ID} template for post-payment lookup
        pass  # Not mandatory but useful
    
    client = get_stripe_client()
    idempotency_key = str(uuid.uuid4())
    
    params: dict[str, Any] = {
        "success_url": success_url,
        "cancel_url": cancel_url,
        "line_items": line_items,
        "mode": mode,
        "payment_method_types": None,  # automatic_payment_methods preferred
        "automatic_payment_methods": {"enabled": True},
    }
    
    if customer_email:
        params["customer_email"] = customer_email
    if metadata:
        params["metadata"] = metadata
    
    try:
        session = client.v1.checkout.sessions.create(
            params=params,
            options={"idempotency_key": idempotency_key},
        )
        return session
    
    except stripe_errors.StripeError as e:
        raise RuntimeError(f"Checkout session failed: {e}") from e


def handle_checkout_completed(session_id: str) -> dict[str, Any]:
    """Verify and fulfill order after checkout.session.completed webhook.

    Call this INSIDE your webhook handler AFTER signature verification.
    Retrieves the session with expanded line_items to see what was purchased.

    Args:
        session_id: The Checkout Session ID from webhook (`cs_...`).

    Returns:
        Dict with session details for order fulfillment.
    """
    client = get_stripe_client()
    
    # Expand line_items to see what was purchased
    session = client.v1.checkout.sessions.retrieve(
        session_id,
        params={"expand": ["line_items", "payment_intent"]},
    )
    
    payment_intent = session.get("payment_intent")
    customer = session.get("customer")
    customer_details = session.get("customer_details", {})
    
    return {
        "session_id": session.id,
        "payment_intent_id": payment_intent.id if payment_intent else None,
        "customer_id": customer if isinstance(customer, str) else customer.id if customer else None,
        "customer_email": customer_details.get("email"),
        "amount_total": session.amount_total,
        "currency": session.currency,
        "status": session.payment_status,  # paid | unpaid | no_payment_required
        "metadata": dict(session.metadata) if session.metadata else {},
        "line_items": [
            {
                "description": item.description,
                "amount_total": item.amount_total,
                "quantity": item.quantity,
            }
            for item in session.line_items.data
        ] if session.line_items else [],
    }

Pattern 3: Webhook Signature Verification (CRITICAL)

"""Webhook signature verification — the ONLY reliable way to know payment succeeded.

Stripe webhooks are SERVER-TO-SERVER callbacks. NEVER trust:
1. Client-side success redirects alone (can be faked)
2. JavaScript success callbacks (can be interrupted)
3. Unverified webhook payloads (can be forged)

Always VERIFY the stripe-signature header BEFORE processing.
"""

from __future__ import annotations

import os
import logging
from typing import Any, Callable
from stripe import Webhook
from stripe import error as stripe_errors

logger = logging.getLogger(__name__)

# Get this from Stripe Dashboard → Developers → Webhooks
# Each endpoint has its OWN webhook secret (not your API secret)
WEBHOOK_SECRET = os.environ.get("STRIPE_WEBHOOK_SECRET", "")


def verify_and_parse_webhook(
    payload: bytes,
    stripe_signature_header: str,
    webhook_secret: str = "",
) -> Any:
    """Verify Stripe webhook signature and parse the event object.

    This is the MOST IMPORTANT part of Stripe integration.

    Args:
        payload: Raw request body bytes (NOT parsed JSON).
        stripe_signature_header: Full `stripe-signature` header value.
        webhook_secret: Secret from Stripe Dashboard Webhooks page.

    Returns:
        Parsed Stripe Event object.

    Raises:
        ValueError: If signature verification fails.
    """
    secret = webhook_secret or WEBHOOK_SECRET
    
    if not secret:
        logger.error("STRIPE_WEBHOOK_SECRET not configured")
        raise ValueError("Webhook secret not configured")
    
    try:
        # ✅ This is how you verify Stripe webhooks
        event = Webhook.construct_event(
            payload,
            stripe_signature_header,
            secret,
        )
        return event
    
    except ValueError as e:
        # Invalid payload or signature
        logger.warning("Webhook payload verification failed: %s", e)
        raise ValueError(f"Invalid webhook payload") from e
    
    except stripe_errors.SignatureVerificationError as e:
        # Signature doesn't match — potential forgery attempt or wrong secret
        logger.warning("Webhook signature verification failed: %s", e)
        raise ValueError(f"Invalid webhook signature") from e


class WebhookRouter:
    """Routes verified webhook events to registered handlers.

    Usage:
        router = WebhookRouter()
        
        @router.on("payment_intent.succeeded")
        def handle_payment_success(event):
            order_id = event.data.object.metadata.get("order_id")
            fulfill_order(order_id)
        
        # In your endpoint:
        event = verify_and_parse_webhook(payload, sig_header)
        router.dispatch(event)
    """
    
    def __init__(self) -> None:
        self._handlers: dict[str, Callable[[Any], None]] = {}
    
    def on(self, event_type: str) -> Callable[[Callable[[Any], None], Callable[[Any], None]]:
        """Decorator to register a handler for an event type."""
        def decorator(handler: Callable[[Any], None]) -> Callable[[Any], None]:
            self._handlers[event_type] = handler
            return handler
        return decorator  # type: ignore[return-value]
    
    def dispatch(self, event: Any) -> bool:
        """Dispatch an event to its registered handler.

        Returns:
            True if handler was called, False if no handler registered.
        """
        handler = self._handlers.get(event.type)
        if handler:
            try:
                handler(event)
                return True
            except Exception:
                logger.exception("Handler failed for event %s: %s", event.type, event.id)
                raise
        return False


# Common events to listen for:
# - payment_intent.succeeded → payment succeeded (reliable!)
# - payment_intent.payment_failed → payment failed / declined
# - checkout.session.completed → checkout done (reliable!)
# - invoice.paid → subscription invoice paid
# - invoice.payment_failed → subscription payment failed
# - customer.subscription.updated → subscription changed
# - charge.refunded → refund processed

# ===================================================================
# ❌ BAD — DO NOT DO THIS
# ===================================================================

def bad_webhook_handler_bad(request_body: str) -> Any:
    """❌ BAD: No signature verification, trusts raw payload parsing."""
    import json
    # ❌ Just parsing JSON — anyone can send fake events!
    event = json.loads(request_body)  # UNSAFE!
    # ❌ This will process forged events too!
    return event


# ===================================================================
# ✅ GOOD — Always verify signature FIRST
# ===================================================================

# Initialize router with common handlers
stripe_router = WebhookRouter()

@stripe_router.on("payment_intent.succeeded")
def on_payment_succeeded(event: Any) -> None:
    """Handle successful payment from verified webhook."""
    intent = event.data.object
    order_id = intent.metadata.get("order_id")
    payment_intent_id = intent.id
    amount_received = intent.amount_received
    currency = intent.currency
    
    logger.info(
        "Payment succeeded: pi=%s order=%s amount=%d%s",
        payment_intent_id,
        order_id,
        amount_received,
        currency,
    )
    
    # ✅ This is where you:
    # 1. Mark order as PAID in YOUR database
    # 2. Send receipt email
    # 3. Fulfill digital goods / unlock features
    # 4. Trigger any post-payment workflows


@stripe_router.on("checkout.session.completed")
def on_checkout_completed(event: Any) -> None:
    """Handle completed checkout session (redirect-based payments)."""
    session = event.data.object
    logger.info("Checkout completed: cs=%s", session.id)
    
    # Retrieve expanded session with line_items if needed
    # Then fulfill order


@stripe_router.on("invoice.paid")
def on_invoice_paid(event: Any) -> None:
    """Handle subscription invoice paid."""
    invoice = event.data.object
    subscription_id = invoice.subscription
    customer_id = invoice.customer
    
    logger.info(
        "Invoice paid: inv=%s sub=%s cust=%s amount_total=%d",
        invoice.id,
        subscription_id,
        customer_id,
        invoice.amount_paid,
    )


@stripe_router.on("payment_intent.payment_failed")
def on_payment_failed(event: Any) -> None:
    """Handle failed/declined payment."""
    intent = event.data.object
    last_payment_error = intent.last_payment_error
    logger.warning(
        "Payment failed: pi=%s code=%s message=%s",
        intent.id,
        last_payment_error.code if last_payment_error else None,
        last_payment_error.message if last_payment_error else None,
    )
    # Mark order as FAILED
    # Notify customer if needed

Pattern 4: Subscriptions with Automatic Retries

"""Stripe Subscriptions API for recurring billing.

Stripe handles:
- Automatic recurring charges
- Proration on plan changes
- Dunning management (retries on failed payments)
- Tax calculation
- Invoicing
"""

from __future__ import annotations

import uuid
from typing import Any
from stripe import StripeClient


def create_subscription(
    customer_id: str,
    price_id: str,
    metadata: dict[str, str] | None = None,
    trial_days: int = 0,
    coupon_id: str | None = None,
) -> Any:
    """Create a subscription for an existing Stripe Customer.

    Args:
        customer_id: Stripe Customer ID (`cus_...`).
        price_id: Stripe Price ID (`price_...`).
        metadata: Optional key-value pairs.
        trial_days: Optional trial period before first charge.
        coupon_id: Optional discount coupon.

    Returns:
        Subscription object.

    Note:
        The Customer should already have a payment method attached,
        or use payment_behavior="default_incomplete" with checkout for SCA.
    """
    client = get_stripe_client()
    idempotency_key = str(uuid.uuid4())
    
    params: dict[str, Any] = {
        "customer": customer_id,
        "items": [{"price": price_id}],
        "payment_behavior": "default_incomplete",  # For SCA-ready
        "expand": ["latest_invoice.payment_intent"],
    }
    
    if trial_days > 0:
        params["trial_period_days"] = trial_days
    if metadata:
        params["metadata"] = metadata
    if coupon_id:
        params["coupon"] = coupon_id
    
    subscription = client.v1.subscriptions.create(
        params=params,
        options={"idempotency_key": idempotency_key},
    )
    
    return subscription


def cancel_subscription(
    subscription_id: str,
    immediate: bool = False,
) -> Any:
    """Cancel a subscription.

    Args:
        subscription_id: Subscription ID (`sub_...`).
        immediate: If True, cancel NOW (prorate refund may apply).
                   If False, cancel at period end (default).
    """
    client = get_stripe_client()
    
    if immediate:
        # Cancel immediately, may issue prorated credit
        return client.v1.subscriptions.cancel(subscription_id)
    else:
        # Cancel at the end of current billing period
        return client.v1.subscriptions.modify(
            subscription_id,
            params={"cancel_at_period_end": True},
        )


def update_subscription_plan(
    subscription_id: str,
    new_price_id: str,
    proration_behavior: str = "create_prorations",
) -> Any:
    """Change a subscription to a different price/plan.

    Args:
        subscription_id: Existing subscription ID.
        new_price_id: New price to switch to.
        proration_behavior: One of:
            - "create_prorations" — charge prorated difference (default)
            - "none" — no proration, change at next period
            - "always_invoice" — invoice immediately
    """
    client = get_stripe_client()
    
    return client.v1.subscriptions.modify(
        subscription_id,
        params={
            "items": [{
                "id": "current",  # Use existing subscription item
                "price": new_price_id,
            }],
            "proration_behavior": proration_behavior,
        },
    )

Constraints

MUST DO

  • Always use StripeClient class (v8+), NOT global stripe.api_key
  • Read STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET from environment variables
  • Use PaymentIntents API (NOT deprecated Charges API)
  • Verify EVERY webhook using Webhook.construct_event() BEFORE processing
  • Add idempotency keys to ALL write operations (create, modify, cancel)
  • Handle requires_action / next_action for 3D Secure flows
  • Listen to webhooks for payment_intent.succeeded, checkout.session.completed
  • Store pi_... / cs_... / sub_... IDs as payment references
  • Use automatic_payment_methods (not payment_method_types=["card"] for broader coverage
  • Implement exponential backoff with jitter for rate limit and network errors

MUST NOT DO

  • NEVER hardcode API keys or webhook secrets in source code
  • NEVER log, print, or store raw card numbers, CVV, or expiry (PCI violation)
  • NEVER trust client-side "success" callbacks without webhook verification
  • NEVER use Charges API (deprecated since 2019; doesn't support SCA)
  • NEVER skip signature verification on webhooks
  • NEVER use the same idempotency key for DIFFERENT operations
  • NEVER parse webhook JSON without construct_event (forgery risk)
  • NEVER expose STRIPE_SECRET_KEY to frontend code
  • NEVER send PII in logs — use metadata.order_id instead of printing full objects

Output Template

When implementing Stripe integrations, produce:

  1. StripeClient Initialization — Client factory reading from env vars, typed error mapping
  2. Payment Flow Choice — One of: PaymentIntent + Elements, or Checkout Session redirect
  3. Webhook Handler — Signature-verified endpoint with router for event types
  4. Error Handling Strategy — Mapping from StripeError subclasses to your exceptions
  5. Database Schema Snippets — Columns for payment_intent_id, status, amount columns
  6. Test Scenarios — Happy path, card decline, requires_action flow

Related Skills

SkillPurpose
coding-paypal-apiPayPal Orders/Subscriptions for alternative payment processor
coding-square-apiSquare for in-person retail and restaurant payments
coding-braintree-apiBraintree (PayPal-owned) for marketplace/PayPal Venmo
coding-adyen-apiAdyen for global enterprise payments
coding-shopify-apiShopify for ecommerce platform with built-in payments

Live References

ResourceURL
Stripe Python SDK (PyPI)https://pypi.org/project/stripe/
Stripe API Referencehttps://stripe.com/docs/api?lang=python
PaymentIntents Quickstarthttps://stripe.com/docs/payments/payment-intents
Checkout Sessions Guidehttps://stripe.com/docs/payments/accept-a-payment
Webhook Signature Verificationhttps://stripe.com/docs/webhooks/signatures
Stripe Python SDK GitHubhttps://github.com/stripe/stripe-python
SCA / 3D Secure Guidehttps://stripe.com/docs/strong-customer-authentication
Subscriptions APIhttps://stripe.com/docs/billing/subscriptions
Stripe Connecthttps://stripe.com/docs/connect
Idempotent Requestshttps://stripe.com/docs/api/idempotent_requests

📎 PCI-DSS Notes

When processing card data:

  • Stripe Elements / Checkout keeps you out of PCI scope (SAQ A)**
  • Never have card numbers touch your server
  • Tokens (pm_...) only
  • Your frontend collects, frontend sends → Stripe returns token → you use token
  • Webhook payment_intent.succeeded = ONLY reliable confirmation

Stripe is Level 1 PCI compliant — using their hosted fields keeps you compliant by proxy.

Skills guardadas