# Subscription utility code
> Ready-to-use code utilities to streamline subscription management in your application. Includes a complete SubscriptionManager class and intelligent retry logic for handling declined payments
This guide contains code snippets to help you manage subscriptions in your application and explanations on their usage.
The guide includes:
* A `SubscriptionManager` class to help you perform subscription operations in your application, such as creating and updating subscriptions.
* A set of examples showing how to implement retry logic for declined subscription payments.
## Subscription manager class
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.
### Constructor
The class is constructed with the following configuration:
The entrypoint value for your Payabli paypoint.
Your Payabli API token.
The environment to use.
### Methods
The class has the following methods:
Creates a new subscription.
Fetches a subscription based on ID.
Updates an existing subscription based on ID.
Deletes a subscription based on ID.
Fetches a list of all subscriptions.
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](/developers/api-reference/subscription/create-a-subscription-or-scheduled-payment) for more information.
### Examples
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.
The `SubscriptionManager` class is framework-agnostic and doesn't have any dependencies. You can use it universally.
```ts TS
type Environment = "sandbox" | "production";
type PaymentMethodCard = {
method: "card";
cardnumber: string;
cardexp: string;
cardcvv?: string | null;
cardzip?: string | null;
cardHolder?: string | null;
initiator?: "payor" | "merchant";
saveIfSuccess?: boolean;
}
type PaymentMethodAch = {
method: "ach";
achAccount: string | null;
achRouting: string | null;
achHolder?: string | null;
achAccountType?: "Checking" | "Savings" | null;
achHolderType?: "personal" | "business" | null;
initiator?: "payor" | "merchant";
achCode: "PPD" | "TEL" | "WEB" | "CCD";
saveIfSuccess?: boolean;
}
type PaymentMethodStored = {
storedMethodId: string;
initiator?: "payor" | "merchant";
storedMethodUsageType?: "unscheduled" | "subscription" | "recurring";
}
type PaymentDetails = {
totalAmount: number;
serviceFee?: number | null;
currency?: string | null;
checkNumber?: string | null;
checkImage?: object | null;
categories?: {
label: string;
amount: number;
description?: string | null;
qty?: number | null;
}[] | null;
splitFunding?: {
recipientEntryPoint?: string | null;
accountId?: string | null;
description?: string | null;
amount?: number | null;
}[] | null;
};
type CustomerData = {
customerId?: number | null;
firstName?: string | null;
lastName?: string | null;
company?: string | null;
customerNumber?: string | null;
billingAddress1?: string | null;
billingAddress2?: string | null;
billingCity?: string | null;
billingState?: string | null;
billingZip?: string | null;
billingCountry?: string | null;
billingPhone?: string | null;
billingEmail?: string | null;
shippingAddress1?: string | null;
shippingAddress2?: string | null;
shippingCity?: string | null;
shippingState?: string | null;
shippingZip?: string | null;
shippingCountry?: string | null;
additionalData?: { [key: string]: any } | null;
identifierFields?: (string | null)[] | null;
};
type InvoiceData = {
invoiceNumber?: string | null; // Max length: 250
invoiceDate?: string | null; // Formats: YYYY-MM-DD, MM/DD/YYYY
invoiceDueDate?: string | null; // Formats: YYYY-MM-DD, MM/DD/YYYY
invoiceType?: 0 | null; // Only 0 is supported
invoiceEndDate?: string | null; // Formats: YYYY-MM-DD, MM/DD/YYYY
invoiceStatus?: 0 | 1 | 2 | 4 | 99 | null; // Status values
frequency?: "one-time" | "weekly" | "every2weeks" | "every6months" | "monthly" | "every3months" | "annually" | null;
paymentTerms?: "PIA" | "CIA" | "UR" | "NET10" | "NET20" | "NET30" | "NET45" | "NET60" | "NET90" | "EOM" | "MFI"
| "5MFI" | "10MFI" | "15MFI" | "20MFI" | "2/10NET30" | "UF" | "10UF" | "20UF" | "25UF" | "50UF" | null;
termsConditions?: string | null;
notes?: string | null;
tax?: number | null;
discount?: number | null;
invoiceAmount?: number | null;
freightAmount?: number | null;
dutyAmount?: number | null;
purchaseOrder?: string | null;
firstName?: string | null;
lastName?: string | null;
company?: string | null;
shippingAddress1?: string | null; // Max length: 250
shippingAddress2?: string | null; // Max length: 100
shippingCity?: string | null; // Max length: 250
shippingState?: string | null;
shippingZip?: string | null; // Max length: 50
shippingCountry?: string | null;
shippingEmail?: string | null; // Max length: 320
shippingPhone?: string | null;
shippingFromZip?: string | null;
summaryCommodityCode?: string | null;
items?: {
itemProductName: string | null; // Max length: 250
itemCost: number;
itemQty: number | null;
itemProductCode?: string | null; // Max length: 250
itemDescription?: string | null; // Max length: 250
itemCommodityCode?: string | null; // Max length: 250
itemUnitOfMeasure?: string | null; // Max length: 100
itemMode?: 0 | 1 | 2 | null;
itemCategories?: (string | null)[] | null;
itemTotalAmount?: number | null;
itemTaxAmount?: number | null;
itemTaxRate?: number | null;
}[] | null;
attachments?: object | null;
additionalData?: { [key: string]: any } | null;
};
type SubscriptionRequest = {
subdomain?: string | null;
source?: string | null;
setPause?: boolean;
paymentMethod: PaymentMethodCard | PaymentMethodAch | PaymentMethodStored;
paymentDetails: PaymentDetails;
customerData: CustomerData;
invoiceData?: InvoiceData;
scheduleDetails: {
planId: number;
startDate: string;
endDate?: string | null;
frequency: string;
};
};
type SubscriptionManagerConfig = {
entryPoint: string;
apiToken: string;
environment?: Environment;
};
class SubscriptionManager {
private baseUrl: string;
private apiToken: string;
private entryPoint: string;
constructor({ entryPoint, apiToken, environment = "sandbox" }: SubscriptionManagerConfig) {
this.baseUrl = this.getBaseUrl(environment);
this.apiToken = apiToken;
this.entryPoint = entryPoint;
}
private getBaseUrl(env: Environment): string {
switch (env) {
case "production":
return "https://api.payabli.com/api";
case "sandbox":
default:
return "https://api-sandbox.payabli.com/api";
}
}
async create(subscription: SubscriptionRequest): Promise {
try {
const response = await fetch(`${this.baseUrl}/Subscription/add`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"requestToken": this.apiToken,
},
body: JSON.stringify({ ...subscription, entryPoint: this.entryPoint }),
});
if (!response.ok) {
const errorData = await response.json();
console.error("Error response:", errorData);
throw new Error(errorData.message || "Failed to create subscription");
}
const res = await response.json();
return res;
} catch (error: any) {
console.error("Error creating subscription:", error.message);
throw error;
}
}
async update(subId: number, subscription: Partial): Promise {
try {
const response = await fetch(`${this.baseUrl}/Subscription/${subId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"requestToken": this.apiToken,
},
body: JSON.stringify({ ...subscription, entryPoint: this.entryPoint }),
});
if (!response.ok) {
const errorData = await response.json();
console.error("Error response:", errorData);
throw new Error(errorData.message || "Failed to update subscription");
}
const res = await response.json();
return res;
} catch (error: any) {
console.error("Error updating subscription:", error.message);
throw error;
}
}
async get(subId: number): Promise {
try {
const response = await fetch(`${this.baseUrl}/Subscription/${subId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"requestToken": this.apiToken,
},
});
if (!response.ok) {
const errorData = await response.json();
console.error("Error response:", errorData);
throw new Error(errorData.message || "Failed to get subscription");
}
const res = await response.json();
return res;
} catch (error: any) {
console.error("Error getting subscription:", error.message);
throw error;
}
}
async list(): Promise {
try {
const response = await fetch(`${this.baseUrl}/Query/subscriptions/${this.entryPoint}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"requestToken": this.apiToken,
},
});
if (!response.ok) {
const errorData = await response.json();
console.error("Error response:", errorData);
throw new Error(errorData.message || "Failed to get list of subscriptions");
}
const res = await response.json();
return res;
} catch (error: any) {
console.error("Error getting list of subscriptions:", error.message);
throw error;
}
}
async delete(subId: number): Promise {
try {
const response = await fetch(`${this.baseUrl}/Subscription/${subId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"requestToken": this.apiToken,
},
});
if (!response.ok) {
const errorData = await response.json();
console.error("Error response:", errorData);
throw new Error(errorData.message || "Failed to delete subscription");
}
const res = await response.json();
return res;
} catch (error: any) {
console.error("Error deleting subscription:", error.message);
throw error;
}
}
}
export default SubscriptionManager;
```
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.
```ts TS
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 API
console.log(sub1Id); // log the response from the API after creating the subscription
console.log(await paysub.get(sub1Id)); // log the details of the created subscription
await paysub.update(sub1Id, {
paymentDetails: {
totalAmount: 150, // update the total amount
},
});
console.log(sub1Id); // log the response from the API after updating the subscription
console.log(await paysub.get(sub1Id)); // log the details of the updated subscription
console.log(await paysub.delete(sub1Id)); // log the response from the API after deleting the subscription
const subList = await paysub.list() // fetch the list of all subscriptions
console.log(subList.Summary); // log the summary of all subscriptions
```
## Subscription retry logic
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.
### Retry flow
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:
Diagram: Subscription Retry Flow Process
This sequence diagram shows how to handle declined subscription payments:
-
Server receives webhook payload
-
Webhook handler checks if Event is
`DeclinedPayment`
-
If not
`DeclinedPayment`
: Stop processing
-
If
`DeclinedPayment`
: Continue to next step
-
Webhook handler queries transaction using
`transId`
from webhook
-
Transaction API returns transaction details
-
Webhook handler checks
`ScheduleReference`
field in transaction
-
If
`ScheduleReference`
is 0 or doesn't exist: Not a subscription payment, stop processing
-
If
`ScheduleReference`
exists: Continue with subscription ID
-
Webhook handler requests subscription details from Subscription API
-
Subscription API returns subscription details
-
Webhook handler updates subscription or retries payment
-
Subscription API confirms operation completed
This flow enables custom retry logic for declined subscription payments. Payabli doesn't automatically retry failed subscription payments.
Set up an endpoint in your server to receive webhooks.
For every webhook received, check if the `Event` field has a value of `DeclinedPayment`.
If the `Event` field has a value of `DeclinedPayment`, query the transaction details using the `transId` field from the webhook payload.
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.
Use the subscription ID to fetch the subscription details.
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.
### Examples
The following examples show how to implement retry logic for declined subscription payments.
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](/guides/pay-ops-developer-notifications-manage).
```js JS
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 structure
const 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 webhooks
const 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();
```
```ts TS
const url: string = "https://api-sandbox.payabli.com/api/Notification";
const headers: Record = {
"requestToken": "o.Se...RnU=", // Replace with your API key
"Content-Type": "application/json"
};
// Base payload structure
interface WebHeaderParameter {
key: string;
value: string;
}
interface Content {
timeZone: string;
webHeaderParameters: WebHeaderParameter[];
eventType: string;
}
interface Payload {
content: Content;
method: string;
frequency: string;
target: string;
status: number;
ownerType: number;
ownerId: string;
}
const basePayload: Payload = {
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 webhooks
const sendWebhook = async (): Promise => {
const payload: Payload = basePayload;
try {
const response: Response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(payload)
});
const responseText: string = 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();
```
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.
```ts TS
// npm install express
// npm install --save-dev @types/express
import express, { Request, Response } from "express";
// Constants for API request
const ENVIRONMENT: "sandbox" | "production" = "sandbox"; // Change as needed
const ENTRY = "your-entry"; // Replace with actual entrypoint value
const API_KEY = "your-api-key"; // Replace with actual API key
// API base URLs based on environment
const API_BASE_URLS = {
sandbox: "https://api-sandbox.payabli.com",
production: "https://api.payabli.com",
};
// Define the expected webhook payload structure
interface WebhookPayload {
Event?: string;
transId?: string;
[key: string]: any; // Allow additional properties
}
// Function to handle declined payments
const handleDeclinedPayment = async (transId?: string): Promise => {
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 => {
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 payloads
app.use(express.json());
// Webhook endpoint
app.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 server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}, Environment: ${ENVIRONMENT}`);
});
```
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](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) documentation for more information.
```ts TS
// use in a Next.js project
// something like /pages/api/webhook-payabli.ts
import { NextApiRequest, NextApiResponse } from "next";
// Constants for API request
const ENVIRONMENT: "sandbox" | "production" = "sandbox"; // Change as needed
const ENTRY = "your-entry"; // Replace with actual entrypoint value
const API_KEY = "your-api-key"; // Replace with actual API key
// API base URLs based on environment
const API_BASE_URLS = {
sandbox: "https://api-sandbox.payabli.com",
production: "https://api.payabli.com",
};
// Define the expected webhook payload structure
interface WebhookPayload {
Event?: string;
transId?: string;
[key: string]: any; // Allow additional properties
}
// Function to handle declined payments
const handleDeclinedPayment = async (transId?: string): Promise => {
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 => {
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`);
}
};
```
## Related resources
See these related resources to help you get the most out of Payabli.
* **[Manage subscriptions with the API](/guides/pay-in-developer-subscriptions-manage)** - Learn how to create, update, and delete your scheduled, subscription, and autopay transactions with the Payabli API