Skip to main content

Documentation Index

Fetch the complete documentation index at: https://lyelpay.mintlify.app/llms.txt

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

The Node.js SDK (@lyel/lyel-pay-node) is designed for server-side environments. Use it to create payment intents from your backend, retrieve their status, and validate incoming webhook events.

Installation

npm install @lyel/lyel-pay-node
Requires Node.js 18+ (uses native fetch and crypto).

Initialization

import { LyelPay } from '@lyel/lyel-pay-node';

const lyel = new LyelPay('YOUR_SECRET_KEY');
You can also pass options:
const lyel = new LyelPay('YOUR_SECRET_KEY', {
  baseUrl: 'https://api.lyelpay.com', // override for custom environments
});
ParameterTypeRequiredDescription
secretKeystringYour secret API key
options.baseUrlstringAPI base URL. Defaults to https://api.lyelpay.com

Payment Intents

paymentIntents.create(params)

Creates a payment intent. Use this to initiate a payment from your server before showing the checkout to your user.
const intent = await lyel.paymentIntents.create({
  amount: '5000',     // string, in smallest currency unit (e.g. FCFA)
  currency: 'XAF',   // 'XAF' | 'XOF'
  description: 'Order #1042 — Boutique Fatou',
  metadata: {
    orderId: '1042',
    customerId: 'cust_abc123',
  },
});

console.log(intent.id);           // 'pi_xxx'
console.log(intent.sessionToken); // pass to frontend for checkout
console.log(intent.status);       // 'PENDING'
console.log(intent.expiresAt);    // ISO timestamp
Parameters:
FieldTypeRequiredDescription
amountstringAmount in smallest unit (e.g. '5000' = 5,000 XAF)
currency'XAF' | 'XOF'Transaction currency
descriptionstringHuman-readable description
metadataRecord<string, unknown>Custom key-value pairs (e.g. order ID, customer ID)
Returns: PaymentIntent
interface PaymentIntent {
  id: string;
  amount: string;
  currency: string;
  status: 'PENDING' | 'COMPLETED' | 'FAILED' | 'EXPIRED';
  mode: 'LIVE' | 'TEST';
  sessionToken: string;
  reference?: string;
  description?: string;
  metadata?: Record<string, unknown>;
  expiresAt: string; // ISO 8601
  createdAt: string; // ISO 8601
}

paymentIntents.retrieve(sessionToken)

Retrieves the current status of a payment intent using its session token.
const intent = await lyel.paymentIntents.retrieve(sessionToken);
console.log(intent.status); // 'PENDING' | 'COMPLETED' | 'FAILED' | 'EXPIRED'
Use retrieve() to poll the status if you’re not using webhooks. For production, webhooks are strongly preferred.

Webhooks

webhooks.constructEvent(payload, header, secret)

Validates an incoming webhook event and returns the parsed event object. Throws if the signature is invalid or the timestamp is too old (> 5 minutes).
// Express example — use raw body middleware
app.post('/webhooks/lyelpay', express.raw({ type: 'application/json' }), (req, res) => {
  const payload = req.body.toString();
  const header = req.headers['lyel-signature'] as string;

  let event: WebhookEvent;

  try {
    event = lyel.webhooks.constructEvent(payload, header, 'YOUR_WEBHOOK_SECRET');
  } catch (err) {
    console.error('Invalid webhook signature:', err.message);
    return res.sendStatus(400);
  }

  switch (event.type) {
    case 'payment.completed':
      await fulfillOrder(event.data.paymentIntent);
      break;
    case 'payment.failed':
      await notifyCustomer(event.data.paymentIntent);
      break;
    case 'payment.expired':
      await cleanupExpiredOrder(event.data.paymentIntent);
      break;
  }

  res.sendStatus(200);
});
You must use a raw body (not parsed JSON) for signature validation to work. Using express.json() before this middleware will break the signature check.
Signature format: The lyel-signature header contains:
t=1716000000,v1=abc123def456...
  • t — Unix timestamp of when the event was sent
  • v1 — HMAC-SHA256 signature of {timestamp}.{payload}
Webhook event types:
TypeDescription
payment.completedPayment was successfully processed
payment.failedPayment attempt failed
payment.expiredPayment intent expired before completion

Error handling

The SDK throws standard Error objects with an added statusCode property:
try {
  const intent = await lyel.paymentIntents.create({ ... });
} catch (err) {
  if (err.statusCode === 401) {
    console.error('Invalid API key');
  } else if (err.statusCode === 422) {
    console.error('Validation error:', err.message);
  } else {
    throw err; // re-throw unexpected errors
  }
}

Full example: SaaS billing flow

import { LyelPay } from '@lyel/lyel-pay-node';
import express from 'express';

const lyel = new LyelPay(process.env.LYELPAY_SECRET_KEY!);
const app = express();

// 1. Create intent when user clicks "Pay"
app.post('/checkout', express.json(), async (req, res) => {
  const { planId, userId } = req.body;
  const plan = await getPlan(planId);

  const intent = await lyel.paymentIntents.create({
    amount: String(plan.price),
    currency: 'XAF',
    description: `Subscription — ${plan.name}`,
    metadata: { planId, userId },
  });

  // Return sessionToken to frontend for checkout UI
  res.json({ sessionToken: intent.sessionToken, intentId: intent.id });
});

// 2. Fulfill on webhook
app.post('/webhooks/lyelpay', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = lyel.webhooks.constructEvent(
    req.body.toString(),
    req.headers['lyel-signature'] as string,
    process.env.LYELPAY_WEBHOOK_SECRET!,
  );

  if (event.type === 'payment.completed') {
    const { metadata } = event.data.paymentIntent;
    await activateSubscription(metadata.userId, metadata.planId);
  }

  res.sendStatus(200);
});