API Reference

Webhook Events

Stripe and PayPal webhook events that Technical Debt Radar processes for subscription and payment lifecycle management.

Webhook Events

Technical Debt Radar processes webhook events from Stripe and PayPal to keep subscription state synchronized. Webhook endpoints do not require JWT authentication --- they use provider-specific signature verification instead.


Stripe Webhooks

Endpoint: POST /billing/webhook/stripe

Signature Verification

Stripe webhooks are verified using HMAC-SHA256. Radar reads the stripe-signature header and validates it against the raw request body using the STRIPE_WEBHOOK_SECRET environment variable.

stripe-signature: t=1679000000,v1=abc123...

The verification uses Stripe's standard signature scheme:

  1. Extract the timestamp (t) and signature (v1) from the header.
  2. Construct the signed payload: {timestamp}.{rawBody}.
  3. Compute HMAC-SHA256 using the webhook secret.
  4. Compare using constant-time comparison to prevent timing attacks.

checkout.session.completed

Fires when: A customer completes the Stripe Checkout payment flow.

How Radar processes it:

  1. Extracts the subscription ID and client_reference_id (organization ID) from the session.
  2. Retrieves the full subscription object from Stripe.
  3. Creates or updates the organization's subscription record in the database.
  4. Provisions plan features and credit allocation.

Payload structure:

{
  "id": "evt_abc123",
  "type": "checkout.session.completed",
  "data": {
    "object": {
      "id": "cs_live_abc123",
      "mode": "subscription",
      "subscription": "sub_xyz789",
      "client_reference_id": "org_x1y2z3",
      "customer": "cus_def456",
      "customer_email": "billing@acme.dev",
      "payment_status": "paid",
      "status": "complete"
    }
  }
}

customer.subscription.updated

Fires when: A subscription is modified --- plan change, billing cycle change, or cancellation scheduled.

How Radar processes it:

  1. Reads the updated subscription status and plan details.
  2. Updates the organization's plan in the database.
  3. Adjusts feature access and credit limits if the plan changed.
  4. If cancel_at_period_end is true, marks the subscription as cancelling.

Payload structure:

{
  "id": "evt_def456",
  "type": "customer.subscription.updated",
  "data": {
    "object": {
      "id": "sub_xyz789",
      "status": "active",
      "cancel_at_period_end": false,
      "current_period_start": 1709251200,
      "current_period_end": 1740873600,
      "items": {
        "data": [
          {
            "price": {
              "id": "price_pro_annual",
              "product": "prod_radar_pro",
              "unit_amount": 46800,
              "recurring": { "interval": "year" }
            }
          }
        ]
      }
    },
    "previous_attributes": {
      "items": { "data": [{ "price": { "id": "price_solo_monthly" } }] }
    }
  }
}

customer.subscription.deleted

Fires when: A subscription is terminated (period ended after cancellation, or immediate cancellation).

How Radar processes it:

  1. Marks the subscription as inactive in the database.
  2. Downgrades the organization to the Free plan.
  3. Zeroes out remaining AI credits.
  4. Disables features not available on Free (PR gate, AI analysis, policy editor, etc.).

Payload structure:

{
  "id": "evt_ghi789",
  "type": "customer.subscription.deleted",
  "data": {
    "object": {
      "id": "sub_xyz789",
      "status": "canceled",
      "ended_at": 1740873600,
      "metadata": {
        "orgId": "org_x1y2z3"
      }
    }
  }
}

invoice.payment_succeeded

Fires when: A recurring invoice is successfully paid.

How Radar processes it:

  1. Logs the payment in the billing history.
  2. Resets the monthly AI credit allocation for the new billing period.
  3. Updates the subscription period dates.

Payload structure:

{
  "id": "evt_jkl012",
  "type": "invoice.payment_succeeded",
  "data": {
    "object": {
      "id": "in_abc123",
      "subscription": "sub_xyz789",
      "customer": "cus_def456",
      "amount_paid": 46800,
      "currency": "usd",
      "status": "paid",
      "period_start": 1740873600,
      "period_end": 1772409600,
      "invoice_pdf": "https://pay.stripe.com/invoice/acct_.../pdf"
    }
  }
}

invoice.payment_failed

Fires when: A recurring payment attempt fails (expired card, insufficient funds, etc.).

How Radar processes it:

  1. Marks the subscription as past_due in the database.
  2. Sends a notification to the organization owner.
  3. Stripe automatically retries the payment per its retry schedule (1, 3, 5, 7 days).
  4. If all retries fail, the subscription is cancelled (triggers customer.subscription.deleted).

Payload structure:

{
  "id": "evt_mno345",
  "type": "invoice.payment_failed",
  "data": {
    "object": {
      "id": "in_def456",
      "subscription": "sub_xyz789",
      "customer": "cus_def456",
      "amount_due": 46800,
      "currency": "usd",
      "status": "open",
      "attempt_count": 1,
      "next_payment_attempt": 1741132800
    }
  }
}

PayPal Webhooks

Endpoint: POST /billing/webhook/paypal

Signature Verification

PayPal webhooks are verified using PayPal's Verify Webhook Signature API. Radar forwards the webhook headers and body to PayPal for server-side verification.

Required headers forwarded for verification:

  • paypal-transmission-id
  • paypal-transmission-time
  • paypal-transmission-sig
  • paypal-cert-url
  • paypal-auth-algo

The verification call:

POST https://api-m.paypal.com/v1/notifications/verify-webhook-signature
{
  "auth_algo": "<paypal-auth-algo>",
  "cert_url": "<paypal-cert-url>",
  "transmission_id": "<paypal-transmission-id>",
  "transmission_sig": "<paypal-transmission-sig>",
  "transmission_time": "<paypal-transmission-time>",
  "webhook_id": "<PAYPAL_WEBHOOK_ID>",
  "webhook_event": { ... }
}

Response must contain "verification_status": "SUCCESS".


BILLING.SUBSCRIPTION.CREATED

Fires when: A new PayPal subscription is created (user approves the subscription).

How Radar processes it:

  1. Extracts the subscription ID and plan details.
  2. Creates the subscription record linked to the organization.
  3. Sets status to pending until activation.

Payload structure:

{
  "id": "WH-abc123",
  "event_type": "BILLING.SUBSCRIPTION.CREATED",
  "resource": {
    "id": "I-PAYPAL_SUB_123",
    "plan_id": "P-PAYPAL_PLAN_PRO",
    "status": "APPROVAL_PENDING",
    "subscriber": {
      "email_address": "billing@acme.dev",
      "name": { "given_name": "Jane", "surname": "Developer" }
    },
    "custom_id": "org_x1y2z3",
    "create_time": "2026-03-18T10:30:00Z"
  }
}

BILLING.SUBSCRIPTION.ACTIVATED

Fires when: The subscription becomes active (first payment processed successfully).

How Radar processes it:

  1. Updates the subscription status to active.
  2. Provisions plan features and credit allocation.
  3. Sets the billing period dates.

Payload structure:

{
  "id": "WH-def456",
  "event_type": "BILLING.SUBSCRIPTION.ACTIVATED",
  "resource": {
    "id": "I-PAYPAL_SUB_123",
    "plan_id": "P-PAYPAL_PLAN_PRO",
    "status": "ACTIVE",
    "custom_id": "org_x1y2z3",
    "billing_info": {
      "next_billing_time": "2026-04-18T10:00:00Z",
      "last_payment": {
        "amount": { "currency_code": "USD", "value": "49.00" },
        "time": "2026-03-18T10:30:00Z"
      }
    }
  }
}

BILLING.SUBSCRIPTION.CANCELLED

Fires when: A PayPal subscription is cancelled (by user or by Radar).

How Radar processes it:

  1. Marks the subscription as cancelled.
  2. Downgrades the organization to the Free plan.
  3. Zeroes out AI credits.
  4. Disables paid features.

Payload structure:

{
  "id": "WH-ghi789",
  "event_type": "BILLING.SUBSCRIPTION.CANCELLED",
  "resource": {
    "id": "I-PAYPAL_SUB_123",
    "status": "CANCELLED",
    "custom_id": "org_x1y2z3",
    "status_update_time": "2026-03-18T10:30:00Z"
  }
}

PAYMENT.SALE.COMPLETED

Fires when: A recurring PayPal payment is successfully completed.

How Radar processes it:

  1. Logs the payment in billing history.
  2. Resets monthly AI credit allocation.
  3. Updates billing period dates.

Payload structure:

{
  "id": "WH-jkl012",
  "event_type": "PAYMENT.SALE.COMPLETED",
  "resource": {
    "id": "PAY-SALE-123",
    "billing_agreement_id": "I-PAYPAL_SUB_123",
    "amount": {
      "total": "49.00",
      "currency": "USD"
    },
    "payment_mode": "INSTANT_TRANSFER",
    "state": "completed",
    "create_time": "2026-04-18T10:00:00Z",
    "custom": "org_x1y2z3"
  }
}

Webhook Security Best Practices

  1. Always verify signatures. Never process webhook payloads without verifying the signature first. Both Stripe and PayPal provide mechanisms for this.

  2. Use raw body. Signature verification requires the raw, unmodified request body. Ensure your server framework provides access to raw bodies (NestJS: enable rawBody in NestFactory.create).

  3. Handle idempotently. Webhook providers may send the same event multiple times. Use the event ID to deduplicate processing.

  4. Respond quickly. Return a 200 response within 5 seconds. Move heavy processing to a background queue.

  5. Monitor failures. Both providers have webhook dashboards showing delivery attempts and failures. Check these if subscriptions are not syncing.

Technical Debt Radar Documentation