Skip to main content

Overview

The stripe-webhook edge function processes Stripe webhook events, specifically handling checkout.session.completed events to finalize token purchases. When a payment succeeds, this function updates the transaction status and credits tokens to the organization.
This endpoint must be registered as a webhook endpoint in your Stripe dashboard. Ensure STRIPE_WEBHOOK_SECRET is configured with the signing secret from Stripe.

Endpoint

POST /functions/v1/stripe-webhook

Authentication

Webhook signature verification via stripe-signature header. No Bearer token required.
stripe-signature: t=1234567890,v1=signature_hash...

Webhook Events Handled

checkout.session.completed

Triggered when a Stripe checkout session is successfully completed. This is the primary event that completes token purchases.

Request

Stripe sends the webhook request with:
  • Raw request body (used for signature verification)
  • stripe-signature header for webhook verification
  • Event data in Stripe’s webhook format

Response

Success Response (200 OK)

received
boolean
Always true to acknowledge receipt of the webhook
{
  "received": true
}

Error Responses

400 Bad Request

Returned when signature is missing or metadata is incomplete.
{
  "error": "Missing signature"
}
{
  "error": "Missing metadata"
}

500 Internal Server Error

Returned when webhook processing fails or configuration is missing.
{
  "error": "Webhook not configured"
}
{
  "error": "Webhook processing failed"
}

Implementation Details

Webhook Signature Verification

From source/supabase/functions/stripe-webhook/index.ts:26-45:
const body = await req.text();
const sig = req.headers.get("stripe-signature");
const webhookSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET");

if (!webhookSecret) {
  console.error("STRIPE_WEBHOOK_SECRET not configured");
  return new Response(
    JSON.stringify({ error: "Webhook not configured" }),
    { status: 500 }
  );
}

if (!sig) {
  return new Response(
    JSON.stringify({ error: "Missing signature" }),
    { status: 400 }
  );
}

const event: Stripe.Event = stripe.webhooks.constructEvent(
  body, 
  sig, 
  webhookSecret
);

Checkout Session Processing

From source/supabase/functions/stripe-webhook/index.ts:47-80:
if (event.type === "checkout.session.completed") {
  const session = event.data.object as Stripe.Checkout.Session;
  const metadata = session.metadata;

  if (!metadata?.organization_id || !metadata?.token_type || !metadata?.quantity) {
    console.error("Missing metadata in checkout session");
    return new Response(
      JSON.stringify({ error: "Missing metadata" }), 
      { status: 400 }
    );
  }

  const orgId = metadata.organization_id;
  const tokenType = metadata.token_type;
  const quantity = parseInt(metadata.quantity, 10);

  // Update transaction status
  await adminClient
    .from("token_transactions")
    .update({ status: "completed" })
    .eq("stripe_session_id", session.id);

  // Credit tokens to organization
  const tokenColumn = tokenType === "event" ? "event_tokens" : "attendee_tokens";
  const { data: org } = await adminClient
    .from("organizations")
    .select(tokenColumn)
    .eq("id", orgId)
    .single();

  if (org) {
    const currentTokens = (org as any)[tokenColumn] || 0;
    await adminClient
      .from("organizations")
      .update({ [tokenColumn]: currentTokens + quantity })
      .eq("id", orgId);
  }
}

Audit Logging

From source/supabase/functions/stripe-webhook/index.ts:82-95:
await adminClient.rpc("log_audit_event", {
  _organization_id: orgId,
  _user_id: metadata.user_id || null,
  _action: `purchased_${tokenType}_tokens`,
  _entity_type: "organization",
  _entity_id: orgId,
  _details: {
    token_type: tokenType,
    quantity,
    amount: metadata.total_amount,
    stripe_session_id: session.id,
  },
});

Checkout Session Metadata

The checkout session created by create-checkout-session includes metadata:
FieldTypeDescription
organization_idUUIDOrganization receiving the tokens
token_typestringEither "event" or "attendee"
quantitystringNumber of tokens purchased
price_per_unitstringPrice per token in decimal format
total_amountstringTotal purchase amount in decimal format
user_idUUIDUser who initiated the purchase

Token Crediting Logic

When a payment succeeds:
  1. Transaction Update: Sets transaction status to "completed"
  2. Token Retrieval: Fetches current token balance for the organization
  3. Token Addition: Adds purchased quantity to existing balance
  4. Database Update: Updates organization’s token column (event_tokens or attendee_tokens)
  5. Audit Log: Records the purchase action with full details

Stripe Configuration

Required Environment Variables

  • STRIPE_SECRET_KEY: Your Stripe secret key
  • STRIPE_WEBHOOK_SECRET: Webhook signing secret from Stripe dashboard

Stripe Dashboard Setup

  1. Go to Stripe Dashboard > Developers > Webhooks
  2. Click “Add endpoint”
  3. Set endpoint URL to: https://<project-ref>.supabase.co/functions/v1/stripe-webhook
  4. Select event: checkout.session.completed
  5. Copy the “Signing secret” and set it as STRIPE_WEBHOOK_SECRET

Security Features

  • Signature Verification: Uses Stripe’s constructEvent to verify webhook authenticity
  • Metadata Validation: Validates required metadata fields before processing
  • Idempotency: Uses stripe_session_id to prevent duplicate processing
  • Service Role: Uses service role key for database operations
  • Audit Trail: Complete audit log of all token purchases

Testing

Use Stripe CLI to test webhooks locally:
stripe listen --forward-to https://<project-ref>.supabase.co/functions/v1/stripe-webhook

stripe trigger checkout.session.completed

Error Handling

The function logs errors to console but returns 200 OK to prevent Stripe from retrying:
try {
  // ... webhook processing
} catch (error) {
  console.error("Webhook error:", error);
  return new Response(
    JSON.stringify({ error: "Webhook processing failed" }),
    { status: 500 }
  );
}
If the function returns a non-2xx status, Stripe will retry the webhook delivery. For transient errors, consider returning 200 and handling retries manually.

Flow Diagram

  1. User completes Stripe checkout session
  2. Stripe sends checkout.session.completed webhook
  3. Function verifies webhook signature
  4. Function extracts metadata from session
  5. Function updates transaction status to “completed”
  6. Function credits tokens to organization
  7. Function logs audit event
  8. Function returns 200 OK to Stripe

Build docs developers (and LLMs) love