Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/own-pay/OwnPay-Documentation/llms.txt

Use this file to discover all available pages before exploring further.

OwnPay’s Node.js integration leverages the native fetch API introduced in Node.js 18, so no external HTTP library is needed. The guide below provides a fully typed TypeScript client, practical Express.js route examples, and a production-grade webhook handler — everything you need to accept payments and react to events in a modern Node.js application.

Prerequisites

Node.js 18+

Required for native fetch support. On Node.js 16 or earlier, substitute fetch with node-fetch or axios.

OwnPay API Key

Generate one in Admin → Developer Hub → API Keys. Keys are shown only once — store yours immediately.
If you’re using ESM ("type": "module" in package.json), use import syntax as shown in the examples. For CommonJS projects, substitute import/export with require/module.exports.

Environment Variables

Store all credentials outside your source tree:
# .env — add to .gitignore
OWNPAY_BASE_URL=https://pay.your-domain.com
OWNPAY_API_KEY=op_live_xxxxxxxxxxxxxxxxxxxxxxxx
OWNPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
APP_URL=https://your-store.com
Load with dotenv or your framework’s built-in env loader (e.g. Next.js automatically reads .env.local).

TypeScript Type Definitions

Define the core types once in a shared module and import them wherever you interact with the OwnPay API:
// ownpay.types.ts

export interface OwnPayResponse<T = unknown> {
  success: boolean
  data: T
  message: string
}

export interface PaymentIntent {
  id: string
  status: 'pending' | 'processing' | 'paid' | 'failed' | 'cancelled' | 'refunded'
  amount: number
  currency: string
  description: string | null
  checkout_url: string
  redirect_url: string
  metadata: Record<string, string>
  created_at: string
  updated_at: string
  paid_at: string | null
}

export interface PaymentLink {
  id: string
  url: string
  amount: number
  currency: string
  description: string | null
  expires_at: string | null
  status: 'active' | 'expired' | 'disabled'
}

export interface CreatePaymentIntentInput {
  amount: number
  currency: string
  description?: string
  customer?: {
    name?: string
    email?: string
    phone?: string
  }
  metadata?: Record<string, string>
  redirect_url: string
  cancel_url?: string
}

export interface CreatePaymentLinkInput {
  amount: number
  currency: string
  description?: string
  expires_at?: string
  redirect_url: string
}

The API Client

// ownpay.client.ts
import type {
  OwnPayResponse,
  PaymentIntent,
  PaymentLink,
  CreatePaymentIntentInput,
  CreatePaymentLinkInput,
} from './ownpay.types.js'

export class OwnPayClient {
  private readonly baseUrl: string
  private readonly apiKey: string

  constructor(baseUrl: string, apiKey: string) {
    this.baseUrl = baseUrl.replace(/\/$/, '') + '/api/v1'
    this.apiKey = apiKey
  }

  async createPaymentIntent(input: CreatePaymentIntentInput): Promise<PaymentIntent> {
    const res = await this.post<PaymentIntent>('payment-intents', input)
    return res.data
  }

  async getPaymentIntent(id: string): Promise<PaymentIntent> {
    const res = await this.get<PaymentIntent>(`payment-intents/${id}`)
    return res.data
  }

  async createPaymentLink(input: CreatePaymentLinkInput): Promise<PaymentLink> {
    const res = await this.post<PaymentLink>('payment-links', input)
    return res.data
  }

  private async post<T>(endpoint: string, body: unknown): Promise<OwnPayResponse<T>> {
    return this.request<T>('POST', endpoint, body)
  }

  private async get<T>(endpoint: string): Promise<OwnPayResponse<T>> {
    return this.request<T>('GET', endpoint)
  }

  private async request<T>(
    method: string,
    endpoint: string,
    body?: unknown,
  ): Promise<OwnPayResponse<T>> {
    const url = `${this.baseUrl}/${endpoint.replace(/^\//, '')}`

    const response = await fetch(url, {
      method,
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Authorization': `Bearer ${this.apiKey}`,
      },
      body: body !== undefined ? JSON.stringify(body) : undefined,
      signal: AbortSignal.timeout(15_000),
    })

    const json = (await response.json()) as OwnPayResponse<T>

    if (!json.success) {
      throw new Error(`OwnPay API error: ${json.message}`)
    }

    return json
  }
}
Initialize the client once at application startup:
import { OwnPayClient } from './ownpay.client.js'

const client = new OwnPayClient(
  process.env.OWNPAY_BASE_URL!,
  process.env.OWNPAY_API_KEY!,
)

Creating a Payment Intent

A payment intent opens a hosted checkout session for a specific amount. After creation, redirect your customer to the returned checkout_url:
// Express.js checkout route
app.post('/checkout', async (req, res) => {
  const { amount, currency, orderId } = req.body

  const intent = await client.createPaymentIntent({
    amount,
    currency,
    description: `Order #${orderId}`,
    customer: {
      name:  req.user.name,
      email: req.user.email,
    },
    metadata: { order_id: String(orderId) },
    redirect_url: `${process.env.APP_URL}/payment/callback`,
    cancel_url:   `${process.env.APP_URL}/payment/cancel`,
  })

  // Save the intent ID before redirecting — you'll need it in the callback
  await db.orders.update(orderId, { payment_intent_id: intent.id })

  res.redirect(intent.checkout_url)
})

Handling the Payment Callback

After payment, OwnPay redirects to your redirect_url with ?intent_id=<id>&status=paid. Always verify status server-side — never trust query parameters alone:
app.get('/payment/callback', async (req, res) => {
  const intentId = req.query.intent_id as string

  if (!intentId) {
    return res.status(400).send('Missing intent_id')
  }

  const intent = await client.getPaymentIntent(intentId)

  if (intent.status === 'paid') {
    const orderId = intent.metadata.order_id
    await fulfillOrder(orderId)
    return res.redirect(`/orders/${orderId}/success`)
  }

  res.redirect('/payment/failed')
})

Payment links are reusable URLs you can share via email, SMS, or a product page — no customer data required up front:
const link = await client.createPaymentLink({
  amount:       5000,
  currency:     'BDT',
  description:  'Monthly subscription',
  expires_at:   new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
  redirect_url: `${process.env.APP_URL}/payment/success`,
})

console.log('Share this link:', link.url)

Webhook Verification

OwnPay signs every webhook with an HMAC-SHA256 signature. The X-OwnPay-Signature header contains sha256=<hex>, computed against timestamp + "." + rawBody.
You must use express.raw() (not express.json()) on your webhook route. If the body is parsed to an object before signature verification, the byte representation changes and the HMAC check fails.
import crypto from 'node:crypto'
import type { Request, Response } from 'express'

function verifyOwnPaySignature(req: Request, secret: string): boolean {
  const signature = req.headers['x-ownpay-signature'] as string | undefined
  const timestamp = req.headers['x-ownpay-timestamp'] as string | undefined

  if (!signature || !timestamp) return false

  // Replay protection: reject events older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false

  const rawBody = req.body as Buffer
  const hmac    = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody.toString()}`)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(`sha256=${hmac}`),
    Buffer.from(signature),
  )
}

// Webhook endpoint — must use express.raw() to preserve the raw body
app.post(
  '/webhooks/ownpay',
  express.raw({ type: 'application/json' }),
  (req: Request, res: Response) => {
    const secret = process.env.OWNPAY_WEBHOOK_SECRET!

    if (!verifyOwnPaySignature(req, secret)) {
      return res.status(401).send('Invalid signature')
    }

    const event = JSON.parse((req.body as Buffer).toString())

    switch (event.event) {
      case 'payment.paid':
        handlePaymentPaid(event.data)
        break
      case 'payment.failed':
        handlePaymentFailed(event.data)
        break
      case 'payment.refunded':
        handlePaymentRefunded(event.data)
        break
      default:
        // Unknown events are safely ignored
        break
    }

    res.status(200).send('OK')
  },
)
Return HTTP 200 as fast as possible. OwnPay marks delivery as failed after 10 seconds and retries with exponential backoff. Defer slow operations (database writes, emails, queued jobs) to an async worker.

Error Handling

The client throws Error for API-level failures and DOMException (name: TimeoutError) for requests that exceed the 15-second timeout:
import type { CreatePaymentIntentInput } from './ownpay.types.js'

async function safeCreateIntent(
  client: OwnPayClient,
  input: CreatePaymentIntentInput,
) {
  try {
    return await client.createPaymentIntent(input)
  } catch (error) {
    if (error instanceof Error) {
      if (error.name === 'TimeoutError') {
        throw new Error('Payment service timed out. Please try again.')
      }
      // API-level error: invalid params, auth failure, etc.
      throw new Error(`Payment initialization failed: ${error.message}`)
    }
    throw error
  }
}

CommonJS Compatibility

If your project uses require instead of import, the same client works in CommonJS after compiling TypeScript or stripping type annotations:
// Plain Node.js with require — no TypeScript
const { OwnPayClient } = require('./ownpay.client.cjs')

const client = new OwnPayClient(
  process.env.OWNPAY_BASE_URL,
  process.env.OWNPAY_API_KEY,
)

Next Steps

Webhooks Guide

All event types, retry behavior, idempotency patterns, and the delivery log API.

PHP Integration

PHP 8.3+ client with stream-based HTTP, payment intents, and webhook verification.

API Reference

Full interactive OpenAPI specification with live request testing.

WooCommerce Plugin

Zero-code OwnPay gateway for WordPress WooCommerce stores.

Build docs developers (and LLMs) love