> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sagescreen.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe webhook handler

> Receives Stripe event webhooks and routes them to the appropriate
handler. Authentication is performed via HMAC-SHA256 signature
verification of the `Stripe-Signature` header against the stored
webhook secret (`ss_stripe_webhook_secret`).

**Signature verification:**
- Header format: `t={timestamp},v1={signature}`
- Signed payload: `{timestamp}.{raw_body}`
- Algorithm: HMAC-SHA256
- Tolerance: 300 seconds (5 minutes)

**Handled event types:**
- `checkout.session.completed` — Creates council for new customers or adds credits for existing
- `invoice.paid` — Handles renewal invoices, adds credits, fires renewal webhook, auto-creates renewal deal for E-SKU
- `charge.refunded` — Deducts credits proportional to refund ratio (never below 0)
- `customer.subscription.deleted` — Marks council as cancelled
- `customer.subscription.updated` — Updates subscription SKU
- `checkout.session.async_payment_succeeded` — Processes cleared async payments (ACH/bank)
- `checkout.session.async_payment_failed` — Cleans up failed async payment data

Unhandled event types are logged and acknowledged with `{received: true}`.




## OpenAPI

````yaml /openapi/private/specs/wp/wp-webhooks.yml post /stripe/webhook
openapi: 3.1.0
info:
  title: >-
    SageScreen — Webhooks & CRM Integrations (Billing · Council · User · Deal ·
    Sage)
  version: 1.0.0
servers:
  - url: https://{domain}/wp-json/sagescreen/v1
    description: WordPress REST API
    variables:
      domain:
        default: api.sagescreen.app
security: []
tags:
  - name: Billing – Stripe
    description: Stripe webhook and subscription endpoints
  - name: Billing – Broadcast
    description: Postmark broadcast unsubscribe endpoint
  - name: Billing – Pipedrive
    description: Pipedrive CRM ID set-* webhook endpoints (called by Zapier)
  - name: Billing – AJAX
    description: Credit ledger, reactivation, and portal AJAX endpoints
  - name: Council – REST
    description: Zapier-facing REST endpoints for council data sync
  - name: Council – AJAX
    description: Council user and sage management AJAX endpoints
  - name: User – REST
    description: Zapier-facing REST endpoints
  - name: User – AJAX
    description: Council user management AJAX endpoints
  - name: User – Profile
    description: Self-service profile updates
  - name: Deal REST
    description: REST API endpoints authenticated via X-API-Key
  - name: Deal AJAX
    description: AJAX endpoints authenticated via WordPress nonce + capability check
  - name: Sage – REST
    description: REST API endpoints for sage operations and Python callbacks
  - name: Sage – CRUD
    description: AJAX endpoints for sage creation, editing, and status management
  - name: Sage – Build Workflow
    description: AJAX endpoints for the multi-step sage build process
paths:
  /stripe/webhook:
    post:
      tags:
        - Billing – Stripe
      summary: Stripe webhook handler
      description: >
        Receives Stripe event webhooks and routes them to the appropriate

        handler. Authentication is performed via HMAC-SHA256 signature

        verification of the `Stripe-Signature` header against the stored

        webhook secret (`ss_stripe_webhook_secret`).


        **Signature verification:**

        - Header format: `t={timestamp},v1={signature}`

        - Signed payload: `{timestamp}.{raw_body}`

        - Algorithm: HMAC-SHA256

        - Tolerance: 300 seconds (5 minutes)


        **Handled event types:**

        - `checkout.session.completed` — Creates council for new customers or
        adds credits for existing

        - `invoice.paid` — Handles renewal invoices, adds credits, fires renewal
        webhook, auto-creates renewal deal for E-SKU

        - `charge.refunded` — Deducts credits proportional to refund ratio
        (never below 0)

        - `customer.subscription.deleted` — Marks council as cancelled

        - `customer.subscription.updated` — Updates subscription SKU

        - `checkout.session.async_payment_succeeded` — Processes cleared async
        payments (ACH/bank)

        - `checkout.session.async_payment_failed` — Cleans up failed async
        payment data


        Unhandled event types are logged and acknowledged with `{received:
        true}`.
      operationId: stripeWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StripeEventPayload'
            example:
              id: evt_1NqIAB2eZvKYlo2C5XFo8Dg6
              type: checkout.session.completed
              data:
                object:
                  id: cs_test_a1b2c3d4
                  customer: cus_Abc123
                  payment_status: paid
                  metadata:
                    council_id: c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6
                    sku: P-25-2
      responses:
        '200':
          description: Event received and processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeWebhookSuccess'
              example:
                received: true
        '400':
          description: Signature verification failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeWebhookError'
              examples:
                missingSignature:
                  summary: No Stripe-Signature header
                  value:
                    error: Missing signature
                invalidFormat:
                  summary: Header cannot be parsed
                  value:
                    error: Invalid signature header format
                timestampExpired:
                  summary: Timestamp outside 5-minute tolerance
                  value:
                    error: Webhook timestamp too old
                signatureMismatch:
                  summary: HMAC does not match
                  value:
                    error: Webhook signature verification failed
      security:
        - stripeSignature: []
components:
  schemas:
    StripeEventPayload:
      type: object
      description: |
        Stripe event JSON object. The full Stripe event envelope is passed
        through; only `type` and `data.object` are consumed by the handler.
      properties:
        id:
          type: string
          description: Stripe event ID
          examples:
            - evt_1NqIAB2eZvKYlo2C5XFo8Dg6
        type:
          $ref: '#/components/schemas/StripeEventType'
        data:
          type: object
          properties:
            object:
              type: object
              description: >-
                The Stripe resource object (session, invoice, charge, or
                subscription)
              additionalProperties: true
      required:
        - id
        - type
        - data
    StripeWebhookSuccess:
      type: object
      required:
        - received
      properties:
        received:
          type: boolean
          const: true
          examples:
            - true
    StripeWebhookError:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable error message
          examples:
            - Missing signature
    StripeEventType:
      type: string
      description: Stripe event types handled by the webhook
      enum:
        - checkout.session.completed
        - invoice.paid
        - charge.refunded
        - customer.subscription.deleted
        - customer.subscription.updated
        - checkout.session.async_payment_succeeded
        - checkout.session.async_payment_failed
      examples:
        - checkout.session.completed
  securitySchemes:
    stripeSignature:
      type: apiKey
      in: header
      name: Stripe-Signature
      description: >
        Stripe webhook signature header. Format:
        `t={unix_timestamp},v1={hmac_sha256_hex}`.

        Verified against `ss_stripe_webhook_secret` with a 5-minute timestamp
        tolerance.

````