Server

Node.js SDK

Server-side Node.js implementation for integrating YardPay Pro. Uses native fetch with automatic retries and Express middleware — no npm packages required.

API Client

Create yardpaypro.mjs (or .js with "type": "module" in package.json)

// yardpaypro.mjs

export class YppError extends Error {
  constructor(status, code, message) {
    super(message);
    this.name = 'YppError';
    this.status = status;
    this.code = code;
  }
}

export class YardPayProClient {
  #apiKey;
  #baseUrl;
  #timeout;
  #retries;
  #retryDelay;

  constructor({ apiKey, baseUrl = process.env.API_BASE_URL || '', timeout = 30000, retries = 2, retryDelay = 1000 }) {
    this.#apiKey = apiKey;
    this.#baseUrl = baseUrl;
    this.#timeout = timeout;
    this.#retries = retries;
    this.#retryDelay = retryDelay;
  }

  get baseUrl() { return this.#baseUrl; }

  async #request(method, path, { body, params } = {}) {
    let url = this.#baseUrl + path;

    if (params) {
      const qs = new URLSearchParams();
      for (const [k, v] of Object.entries(params)) {
        if (v !== undefined && v !== null) qs.append(k, String(v));
      }
      const str = qs.toString();
      if (str) url += '?' + str;
    }

    let lastError;
    for (let attempt = 0; attempt <= this.#retries; attempt++) {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), this.#timeout);

      try {
        const res = await fetch(url, {
          method,
          headers: {
            'Content-Type': 'application/json',
            'x-api-key': this.#apiKey,
          },
          body: body ? JSON.stringify(body) : undefined,
          signal: controller.signal,
        });
        clearTimeout(timer);

        if (res.status >= 500 && attempt < this.#retries) {
          lastError = new YppError(res.status, 'server_error', res.statusText);
          await new Promise(r => setTimeout(r, this.#retryDelay * (attempt + 1)));
          continue;
        }

        if (!res.ok) {
          const err = await res.json().catch(() => ({}));
          throw new YppError(res.status, err.code || 'api_error', err.message || res.statusText);
        }

        if (res.status === 204) return null;

        const data = await res.json();
        return data.value !== undefined ? data.value : data;
      } catch (err) {
        clearTimeout(timer);
        if (err instanceof YppError) throw err;
        if (err.name === 'AbortError') {
          lastError = new YppError(0, 'timeout', 'Request timed out');
          if (attempt < this.#retries) {
            await new Promise(r => setTimeout(r, this.#retryDelay * (attempt + 1)));
            continue;
          }
        }
        throw lastError || err;
      }
    }
    throw lastError;
  }

  get(path, params) { return this.#request('GET', path, { params }); }
  post(path, body) { return this.#request('POST', path, { body }); }
  put(path, body) { return this.#request('PUT', path, { body }); }
  delete(path) { return this.#request('DELETE', path); }
}
Services

Create yardpaypro-services.mjs

// yardpaypro-services.mjs

export class Payments {
  #client;
  constructor(client) { this.#client = client; }

  payInvoice(data) {
    return this.#client.post('/api/payment/payinvoice', data);
  }
  getPaymentMethods(invoiceId) {
    return this.#client.get(`/v1/invoice/${invoiceId}/PaymentMethods`);
  }
  getFundingSources() {
    return this.#client.get('/v1/FundingSource');
  }
  registerCard({ card, name }) {
    return this.#client.post('/v1/payment/RegisterCard', { card, name });
  }
  verifyCard(fundingSourceId, code) {
    return this.#client.post(`/v1/fundingSource/${fundingSourceId}/verify`, { VerificationCode: code });
  }
}

export class Invoices {
  #client;
  constructor(client) { this.#client = client; }

  getByReference(code) {
    return this.#client.get(`/v1/invoice/getByReference(code='${code}')`);
  }
  getById(id, expand) {
    return this.#client.get(`/v1/invoice/${id}`, expand ? { $expand: expand } : undefined);
  }
  list({ filter, orderBy, top = 20, skip = 0, expand } = {}) {
    return this.#client.get('/v1/invoice', {
      $filter: filter, $orderby: orderBy, $top: top, $skip: skip, $expand: expand,
    });
  }
  create(data) {
    return this.#client.post('/v1/invoice', data);
  }
  getQrCodeUrl(identifier) {
    return `${this.#client.baseUrl}/v1/invoice/qrcode/${identifier}`;
  }
}

export class Subscriptions {
  #client;
  constructor(client) { this.#client = client; }

  // Products
  listProducts(params) { return this.#client.get('/v1/SubscriptionProduct', params); }
  createProduct(data) { return this.#client.post('/v1/SubscriptionProduct', data); }
  updateProduct(id, data) { return this.#client.put(`/v1/SubscriptionProduct/${id}`, data); }
  deleteProduct(id) { return this.#client.delete(`/v1/SubscriptionProduct/${id}`); }

  // Plans
  listPlans(params) { return this.#client.get('/v1/SubscriptionPlan', params); }
  createPlan(data) { return this.#client.post('/v1/SubscriptionPlan', data); }
  updatePlan(id, data) { return this.#client.put(`/v1/SubscriptionPlan/${id}`, data); }
  deletePlan(id) { return this.#client.delete(`/v1/SubscriptionPlan/${id}`); }

  // Inventory
  listInventory(params) { return this.#client.get('/v1/SubscriptionInventoryItem', params); }
  createInventoryItem(data) { return this.#client.post('/v1/SubscriptionInventoryItem', data); }
  updateInventoryItem(id, data) { return this.#client.put(`/v1/SubscriptionInventoryItem/${id}`, data); }
  deleteInventoryItem(id) { return this.#client.delete(`/v1/SubscriptionInventoryItem/${id}`); }
}
Express.js Integration
// server.mjs

import express from 'express';
import { YardPayProClient, YppError } from './yardpaypro.mjs';
import { Payments, Invoices } from './yardpaypro-services.mjs';

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

const client = new YardPayProClient({
  apiKey: process.env.YARDPAYPRO_API_KEY,
});
const payments = new Payments(client);
const invoices = new Invoices(client);

// Create invoice endpoint
app.post('/api/invoices', async (req, res) => {
  try {
    const invoice = await invoices.create(req.body);
    res.json(invoice);
  } catch (err) {
    if (err instanceof YppError) {
      res.status(err.status || 500).json({ error: err.code, message: err.message });
    } else {
      res.status(500).json({ error: 'internal', message: err.message });
    }
  }
});

// Pay invoice endpoint
app.post('/api/payments', async (req, res) => {
  try {
    const result = await payments.payInvoice(req.body);
    res.json(result);
  } catch (err) {
    if (err instanceof YppError) {
      res.status(err.status || 500).json({ error: err.code, message: err.message });
    } else {
      res.status(500).json({ error: 'internal', message: err.message });
    }
  }
});

app.listen(3000, () => console.log('Server running on :3000'));
Webhook Middleware
// webhook-middleware.mjs

import crypto from 'node:crypto';

/**
 * Express middleware that verifies the webhook signature
 * and attaches the parsed event to req.webhookEvent.
 */
export function webhookMiddleware({ secret }) {
  return (req, res, next) => {
    const signature = req.headers['x-ypp-signature'];
    if (!signature) {
      return res.status(401).json({ error: 'Missing signature' });
    }

    const payload = typeof req.body === 'string'
      ? req.body
      : JSON.stringify(req.body);

    const expected = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    req.webhookEvent = typeof req.body === 'string'
      ? JSON.parse(req.body)
      : req.body;

    next();
  };
}

// Usage:
app.post(
  '/webhooks',
  express.raw({ type: 'application/json' }),
  webhookMiddleware({ secret: process.env.YARDPAYPRO_WEBHOOK_SECRET }),
  (req, res) => {
    const event = req.webhookEvent;

    switch (event.type) {
      case 'payment.completed':
        console.log('Payment completed:', event.data.referenceNumber);
        break;
      case 'invoice.paid':
        console.log('Invoice paid:', event.data.id);
        break;
    }

    res.sendStatus(200);
  }
);
Full Example
import { YardPayProClient, YppError } from './yardpaypro.mjs';
import { Payments, Invoices, Subscriptions } from './yardpaypro-services.mjs';

const client = new YardPayProClient({
  apiKey: process.env.YARDPAYPRO_API_KEY,
  retries: 3,        // auto-retry on 5xx and timeouts
  retryDelay: 1000,  // 1s, 2s, 3s backoff
});

const payments = new Payments(client);
const invoices = new Invoices(client);
const subs = new Subscriptions(client);

// --- Full payment flow ---
const invoice = await invoices.create({
  description: 'Order #1234',
  currencyCode: 'JMD',
  items: [{ name: 'Widget', unitCost: 100, quantity: 2, invoiceItemTypeId: 'Product' }],
  sourceInfo: { firstName: 'Test', lastName: 'User', email: '[email protected]' },
});

const methods = await payments.getPaymentMethods(invoice.id);
const cardMethod = methods.find(m => m.paymentMethodId === 'CreditCard');

try {
  const result = await payments.payInvoice({
    invoiceIdentifier: invoice.identifier,
    paymentOptionId: cardMethod.id,
    card: { name: 'Test User', cardNumber: '4111111111111111', expiryDate: '12/28', cvv: '123' },
  });
  console.log('Paid:', result.referenceNumber);
} catch (err) {
  if (err instanceof YppError) {
    console.error(`[${err.status}] ${err.code}: ${err.message}`);
  }
}

// --- Subscription setup ---
const product = await subs.createProduct({
  name: 'Business Suite',
  subscriptionProductType: 'Service',
});
await subs.createPlan({
  productId: product.id,
  name: 'Monthly',
  amount: '29.99',
  currency: 'USD',
  timeUnit: 'Month',
  timeInterval: 1,
  subscriptionBillingType: 'Recurring',
  status: 'Active',
});
Requirements
  • Node.js 18+ (native fetch)
  • No npm packages required — uses native fetch, crypto, and AbortController
  • Express.js for the middleware examples (optional — services work standalone)
  • ESM modules (.mjs or "type": "module" in package.json)