Skip to main content
The WebhookPayloadSchemas export provides TypeScript type definitions for all webhook event payloads sent by PayNow.

Overview

Webhook schemas are automatically extracted from the PayNow OpenAPI specification and provide type safety for handling webhook events in your application.
import type { WebhookPayloadSchemas } from 'paynow';

type SubscriptionRenewed = WebhookPayloadSchemas['ON_SUBSCRIPTION_RENEWED'];
type OrderCompleted = WebhookPayloadSchemas['ON_ORDER_COMPLETED'];
type RefundIssued = WebhookPayloadSchemas['ON_REFUND'];

Using Webhook Schemas

Typing Webhook Handlers

Use WebhookPayloadSchemas to type your webhook event handlers:
import type { WebhookPayloadSchemas } from 'paynow';
import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhooks/paynow', (req, res) => {
  const payload = req.body;
  
  switch (payload.event_type) {
    case 'ON_ORDER_COMPLETED': {
      const event = payload as WebhookPayloadSchemas['ON_ORDER_COMPLETED'];
      console.log(`Order ${event.body.id} completed`);
      console.log(`Total: ${event.body.total_amount} ${event.body.currency}`);
      break;
    }
    
    case 'ON_SUBSCRIPTION_RENEWED': {
      const event = payload as WebhookPayloadSchemas['ON_SUBSCRIPTION_RENEWED'];
      console.log(`Subscription ${event.body.id} renewed`);
      console.log(`Billing cycle: ${event.body.billing_cycle_sequence}`);
      break;
    }
    
    case 'ON_REFUND': {
      const event = payload as WebhookPayloadSchemas['ON_REFUND'];
      console.log(`Refund issued for checkout ${event.body.checkout_id}`);
      break;
    }
  }
  
  res.status(200).send('OK');
});

Type-Safe Event Processing

Create type-safe webhook processors:
import type { WebhookPayloadSchemas } from 'paynow';

type WebhookHandler<T extends keyof WebhookPayloadSchemas> = (
  payload: WebhookPayloadSchemas[T]
) => Promise<void>;

const handleOrderCompleted: WebhookHandler<'ON_ORDER_COMPLETED'> = async (payload) => {
  const { event_id, body } = payload;
  
  // Process the order
  await fulfillOrder({
    orderId: body.id,
    customerId: body.customer_id,
    items: body.lines,
    total: body.total_amount
  });
  
  // Log the event
  console.log(`Processed order completion event ${event_id}`);
};

const handleSubscriptionRenewed: WebhookHandler<'ON_SUBSCRIPTION_RENEWED'> = async (payload) => {
  const { body } = payload;
  
  // Update subscription status
  await updateSubscriptionStatus({
    subscriptionId: body.id,
    billingCycle: body.billing_cycle_sequence,
    nextBillingDate: body.current_period_end
  });
};

Creating a Webhook Router

Build a type-safe webhook router:
import type { WebhookPayloadSchemas } from 'paynow';

type WebhookEventType = keyof WebhookPayloadSchemas;

class WebhookRouter {
  private handlers = new Map<WebhookEventType, (payload: any) => Promise<void>>();
  
  on<T extends WebhookEventType>(
    eventType: T,
    handler: (payload: WebhookPayloadSchemas[T]) => Promise<void>
  ) {
    this.handlers.set(eventType, handler);
    return this;
  }
  
  async handle(payload: any) {
    const handler = this.handlers.get(payload.event_type);
    if (handler) {
      await handler(payload);
    }
  }
}

// Usage
const router = new WebhookRouter();

router
  .on('ON_ORDER_COMPLETED', async (payload) => {
    // payload is properly typed as WebhookPayloadSchemas['ON_ORDER_COMPLETED']
    console.log(`Order ${payload.body.id} completed`);
  })
  .on('ON_SUBSCRIPTION_RENEWED', async (payload) => {
    // payload is properly typed as WebhookPayloadSchemas['ON_SUBSCRIPTION_RENEWED']
    console.log(`Subscription ${payload.body.id} renewed`);
  })
  .on('ON_REFUND', async (payload) => {
    // payload is properly typed as WebhookPayloadSchemas['ON_REFUND']
    console.log(`Refund issued`);
  });

app.post('/webhooks/paynow', async (req, res) => {
  await router.handle(req.body);
  res.status(200).send('OK');
});

Webhook Event Types

The following webhook events are available:

Order Events

ON_ORDER_COMPLETED
object
Triggered when an order is completed and payment is successful

Subscription Events

ON_SUBSCRIPTION_ACTIVATED
object
Triggered when a subscription becomes active
ON_SUBSCRIPTION_RENEWED
object
Triggered when a subscription is renewed for a new billing cycle
ON_SUBSCRIPTION_CANCELED
object
Triggered when a subscription is canceled

Trial Events

ON_TRIAL_ACTIVATED
object
Triggered when a trial period starts
ON_TRIAL_COMPLETED
object
Triggered when a trial period ends successfully
ON_TRIAL_CANCELED
object
Triggered when a trial is canceled

Delivery Events

ON_DELIVERY_ITEM_ADDED
object
Triggered when a delivery item is added to a customer’s inventory
ON_DELIVERY_ITEM_ACTIVATED
object
Triggered when a delivery item is activated
ON_DELIVERY_ITEM_USED
object
Triggered when a delivery item is used/consumed
ON_DELIVERY_ITEM_RENEWED
object
Triggered when a delivery item is renewed
ON_DELIVERY_ITEM_EXPIRED
object
Triggered when a delivery item expires
ON_DELIVERY_ITEM_REVOKED
object
Triggered when a delivery item is revoked

Payment Events

ON_REFUND
object
Triggered when a refund is issued
ON_CHARGEBACK
object
Triggered when a chargeback is initiated
ON_CHARGEBACK_CLOSED
object
Triggered when a chargeback is closed

Other Events

ON_DISCORD_ACCOUNT_LINKED_TO_CHECKOUT
object
Triggered when a Discord account is linked to a checkout

Common Webhook Payload Structure

All webhook payloads share a common structure:
event_type
string
required
The type of webhook event (e.g., ON_ORDER_COMPLETED)
event_id
string
required
The unique Flake ID of the webhook event for idempotency
body
object
required
The event-specific payload data

Example: ON_SUBSCRIPTION_RENEWED Payload

{
  "event_type": "ON_SUBSCRIPTION_RENEWED",
  "event_id": "411486491630370816",
  "body": {
    "id": "411486491630370816",
    "store_id": "411486491630370816",
    "customer_id": "411486491630370816",
    "customer": { /* CustomerDTO */ },
    "checkout": { /* CheckoutDTO */ },
    "billing_cycle_sequence": 2,
    "billing_email": "[email protected]",
    "subtotal_amount": 8999,
    "tax_amount": 1350,
    "discount_amount": 0,
    "total_amount": 10349,
    "currency": "USD",
    "interval_value": 1,
    "interval_scale": "month",
    "product_id": "411486491630370816",
    "product_version_id": "411486491630370816",
    "product_name": "Premium Subscription",
    "product_image_url": "https://example.com/image.jpg",
    "product": { /* ProductDTO */ },
    "billing_country": "US",
    "initial_subtotal_amount": 8999,
    "initial_tax_amount": 1350,
    "initial_discount_amount": 1500,
    "initial_giftcard_usage_amount": 0,
    "initial_total_amount": 8849,
    "customer_ip": "127.0.0.1",
    "current_period_start": "2024-03-01T00:00:00Z",
    "current_period_end": "2024-06-01T00:00:00Z",
    "created_at": "2024-03-01T11:00:00Z",
    "active_at": "2024-03-01T11:05:00Z",
    "checkout_id": "411486491630370816",
    "checkout_line_id": "411486491630370816",
    "status": "active"
  }
}

Schema Extraction

The WebhookPayloadSchemas type is derived from webhook request bodies:
import type { webhooks as Webhooks } from './generated/webhooks';
import type { ExtractJsonRequestBodies } from './types';

export type WebhookPayloadSchemas = ExtractJsonRequestBodies<Webhooks>;
The ExtractJsonRequestBodies utility type extracts the application/json request body type from each webhook definition, providing clean types for each event.

Best Practices

Use the event_id field to implement idempotent webhook processing and prevent duplicate event handling.
const processedEvents = new Set<string>();

async function handleWebhook(payload: any) {
  if (processedEvents.has(payload.event_id)) {
    return; // Already processed
  }
  
  // Process the event
  await processEvent(payload);
  
  // Mark as processed
  processedEvents.add(payload.event_id);
}
Use type guards for runtime validation:
function isOrderCompletedEvent(
  payload: any
): payload is WebhookPayloadSchemas['ON_ORDER_COMPLETED'] {
  return payload.event_type === 'ON_ORDER_COMPLETED';
}

if (isOrderCompletedEvent(payload)) {
  // TypeScript knows payload is ON_ORDER_COMPLETED
  console.log(payload.body.total_amount);
}
Always return a 200 response to acknowledge receipt, even if processing fails:
app.post('/webhooks', async (req, res) => {
  try {
    await processWebhook(req.body);
  } catch (error) {
    // Log error but still acknowledge
    console.error('Webhook processing failed:', error);
    // Add to retry queue if needed
  }
  
  res.status(200).send('OK');
});

Webhook Configuration

Learn how to configure webhooks in your store

Management Schemas

Type definitions for Management API

Storefront Schemas

Type definitions for Storefront API

Build docs developers (and LLMs) love