The SubscriptionManager class is a utility class that helps you manage subscriptions in your code.
It has methods to create, read, update, delete, and query subscriptions from the API.
You can use the class in TypeScript or JavaScript projects.
The SubscriptionManager class uses your private Payabli API token to authenticate API requests.
Make sure you keep your API token secure and do not expose it in your client-side code.
The SubscriptionRequest type matches the structure of the request body for creating a subscription but doesn’t need an entryPoint to be manually defined.
See Create a Subscription, Scheduled Payment, or Autopay for more information.
The class implementation contains the SubscriptionManager class and the types used in the class.
The usage example initializes the class and shows how to create, update, get, and delete a subscription.
Class Implementation
The SubscriptionManager class is framework-agnostic and doesn’t have any dependencies. You can use it universally.
This example uses the SubscriptionManager class to make API calls for creating, updating, getting, and deleting subscriptions.
See the comments in the code to understand how each method is used.
Copy
const paysub = new SubscriptionManager({ entryPoint: "A123456789", // replace with your entrypoint apiToken: "o.Se...RnU=", // replace with your API key environment: "sandbox"});const sub1 = await paysub.create({ paymentMethod: { method: "card", initiator: "payor", cardHolder: "John Cassian", cardzip: "12345", cardcvv: "996", cardexp: "12/34", cardnumber: "6011000993026909", }, paymentDetails: { totalAmount: 100, serviceFee: 0, }, customerData: { customerId: 4440, }, scheduleDetails: { planId: 1, startDate: "05-20-2025", endDate: "05-20-2026", frequency: "weekly" }})const sub1Id = sub1.responseData; // hold the subscription ID returned from the APIconsole.log(sub1Id); // log the response from the API after creating the subscriptionconsole.log(await paysub.get(sub1Id)); // log the details of the created subscriptionawait paysub.update(sub1Id, { paymentDetails: { totalAmount: 150, // update the total amount },});console.log(sub1Id); // log the response from the API after updating the subscriptionconsole.log(await paysub.get(sub1Id)); // log the details of the updated subscriptionconsole.log(await paysub.delete(sub1Id)); // log the response from the API after deleting the subscriptionconst subList = await paysub.list() // fetch the list of all subscriptionsconsole.log(subList.Summary); // log the summary of all subscriptions
Sometimes a subscription payment may fail for various reasons, such as insufficient funds, an expired card, or other issues.
When a subscription payment declines, you may want to retry the payment or take other actions to ensure the subscription remains active, such as contacting the customer.
Payabli doesn’t retry failed subscription payments automatically, but you can follow this guide to implement your own retry logic for declined subscription payments.
Before you can receive webhook notifications for declined payments, you need to create a notification for the DeclinedPayment event.
After creating the notification, you can listen for the event in your server and implement the retry logic.
Build retry logic based on this flow:
1
Receive Webhook
Set up an endpoint in your server to receive webhooks.
2
Listen for DeclinedPayment
For every webhook received, check if the Event field has a value of DeclinedPayment.
3
Fetch Transaction
If the Event field has a value of DeclinedPayment, query the transaction details using the transId field from the webhook payload.
4
Check for Subscription
From the transaction details, fetch the subscription ID which is stored in the ScheduleReference field.
If this value is 0 or not found, this declined payment isn’t associated with a subscription.
5
Fetch Subscription
Use the subscription ID to fetch the subscription details.
6
Operate on Subscription
Use the subscription ID to perform business logic.
Some examples include: updating the subscription with a new payment method, retrying the payment, or notifying the customer.
This section covers two examples for implementing retry logic for declined subscription payments:
Express.js: A single-file program using Express.js.
Next.js: A Next.js API route.
Both examples respond to the DeclinedPayment event for declined subscription payments and update the subscription to use a different payment method.
The following examples show how to implement retry logic for declined subscription payments.
Create DeclinedPayment Notification
Before implementing the retry logic, you need to create a webhook notification for the DeclinedPayment event.
After the notification is created, you can listen for the event in a server and implement the retry logic.
For more information, see Manage Notifications.
Copy
const url = "https://api-sandbox.payabli.com/api/Notification";const headers = { "requestToken": "o.Se...RnU=", // Replace with your API key "Content-Type": "application/json"};// Base payload structureconst basePayload = { content: { timeZone: "-5", webHeaderParameters: [ // Replace with your own authentication parameters { key: "myAuthorizationID", value: "1234" } ], eventType: "DeclinedPayment", }, method: "web", frequency: "untilcancelled", target: "https://my-app-url.com/", // Replace with your own URL status: 1, ownerType: 2, ownerId: "255" // Replace with your own paypoint ID};// Function to send webhooksconst sendWebhook = async () => { const payload = basePayload; try { const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload) }); const responseText = await response.text(); console.log(`Notification for DeclinedPayment, Status: ${response.status}, Response: ${responseText}`); } catch (error) { console.error(`Failed to create notification for DeclinedPayment:`, error); }};sendWebhook();
Express.js Single-File Example
The Express.js example can be used as a standalone server in a server-side JavaScript or TypeScript runtime such as Node, Bun, or Deno.
Copy
// npm install express // npm install --save-dev @types/expressimport express, { Request, Response } from "express";// Constants for API requestconst ENVIRONMENT: "sandbox" | "production" = "sandbox"; // Change as neededconst ENTRY = "your-entry"; // Replace with actual entrypoint valueconst API_KEY = "your-api-key"; // Replace with actual API key// API base URLs based on environmentconst API_BASE_URLS = { sandbox: "https://api-sandbox.payabli.com", production: "https://api.payabli.com",};// Define the expected webhook payload structureinterface WebhookPayload { Event?: string; transId?: string; [key: string]: any; // Allow additional properties}// Function to handle declined paymentsconst handleDeclinedPayment = async (transId?: string): Promise<void> => { if (!transId) { console.log("DeclinedPayment received, but it didn't include a transaction ID."); return Promise.resolve(); } // Fetch transaction from transId in DeclinedPayment event const transactionQueryUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Query/transactions/${ENTRY}?transId(eq)=${transId}`; const headers = { requestToken: API_KEY }; // Get subscription ID from transaction return fetch(transactionQueryUrl, { method: "GET", headers }) .then(res => res.ok ? res.json() : Promise.reject(`HTTP ${res.status}: ${res.statusText}`)) .then(data => { const subscriptionId = data?.Records[0]?.ScheduleReference; if (!subscriptionId) { console.log("DeclinedPayment notification received, but no subscription ID found."); return; } return subscriptionRetry(subscriptionId); // Perform logic on subscription with subscription ID }) .catch(error => console.error(`Error handling declined payment: ${error}`));};const subscriptionRetry = async (subId: string): Promise<void> => { const subscriptionUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Subscription/${subId}`; const headers = { "Content-Type": "application/json", requestToken: API_KEY, }; const body = JSON.stringify({ setPause: false, // unpause subscription after decline paymentDetails: { storedMethodId: "4000e8c6-...-1323", // Replace with actual stored method ID storedMethodUsageType: "recurring", }, scheduleDetails: { startDate: "2025-05-20", // Must be a future date }, }); return fetch(subscriptionUrl, { method: "PUT", headers, body }) .then(response => !response.ok ? Promise.reject(`HTTP ${response.status}: ${response.statusText}`) : response.json()) .then(data => console.log("Subscription updated successfully:", data)) .catch(error => console.error("Error updating subscription:", error));};const app = express();const PORT = 3333;// Middleware to parse JSON payloadsapp.use(express.json());// Webhook endpointapp.post("/webhook", (req: Request, res: Response): void => { const payload: WebhookPayload = req.body; if (payload.Event === "DeclinedPayment") { handleDeclinedPayment(payload.transId); } res.sendStatus(200); // Acknowledge receipt});// Start serverapp.listen(PORT, () => { console.log(`Server is running on port ${PORT}, Environment: ${ENVIRONMENT}`);});
Next.js API Route Example
The Next.js example can’t be used as a standalone server but can be dropped into a Next.js project. See the Next.js API Routes documentation for more information.
Copy
// use in a Next.js project// something like /pages/api/webhook-payabli.tsimport { NextApiRequest, NextApiResponse } from "next";// Constants for API requestconst ENVIRONMENT: "sandbox" | "production" = "sandbox"; // Change as neededconst ENTRY = "your-entry"; // Replace with actual entrypoint valueconst API_KEY = "your-api-key"; // Replace with actual API key// API base URLs based on environmentconst API_BASE_URLS = { sandbox: "https://api-sandbox.payabli.com", production: "https://api.payabli.com",};// Define the expected webhook payload structureinterface WebhookPayload { Event?: string; transId?: string; [key: string]: any; // Allow additional properties}// Function to handle declined paymentsconst handleDeclinedPayment = async (transId?: string): Promise<void> => { if (!transId) { console.log("DeclinedPayment notification received, but it didn't include a transaction ID."); return Promise.resolve(); } // Fetch transaction from transId in DeclinedPayment event const transactionQueryUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Query/transactions/${ENTRY}?transId(eq)=${transId}`; const headers = { requestToken: API_KEY }; // Get subscription ID from transaction return fetch(transactionQueryUrl, { method: "GET", headers }) .then(res => res.ok ? res.json() : Promise.reject(`HTTP ${res.status}: ${res.statusText}`)) .then(data => { const subscriptionId = data?.Records[0]?.ScheduleReference; if (!subscriptionId) { console.log("DeclinedPayment notification received, but no subscription ID found."); return; } return subscriptionRetry(subscriptionId); // Perform logic on subscription with subscription ID }) .catch(error => console.error(`Error handling declined payment: ${error}`));};const subscriptionRetry = async (subId: string): Promise<void> => { const subscriptionUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Subscription/${subId}`; const headers = { "Content-Type": "application/json", requestToken: API_KEY, }; const body = JSON.stringify({ setPause: false, // unpause subscription after decline paymentDetails: { storedMethodId: "4000e8c6-...-1323", // Replace with actual stored method ID storedMethodUsageType: "recurring", }, scheduleDetails: { startDate: "2025-05-20", // Must be a future date }, }); return fetch(subscriptionUrl, { method: "PUT", headers, body }) .then(response => !response.ok ? Promise.reject(`HTTP ${response.status}: ${response.statusText}`) : response.json()) .then(data => console.log("Subscription updated successfully:", data)) .catch(error => console.error("Error updating subscription:", error));};export default (req: NextApiRequest, res: NextApiResponse): void => { if (req.method === "POST") { const payload: WebhookPayload = req.body; if (payload.Event === "DeclinedPayment") { handleDeclinedPayment(payload.transId); } res.status(200).end(); // Acknowledge receipt } else { res.setHeader("Allow", ["POST"]); res.status(405).end(`Method ${req.method} Not Allowed`); }};