Subscription utility code

Use example code to enhance your subscription management via the API

Applies to:Developers

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:

entryPoint
string

The entrypoint value for your Payabli paypoint.

apiToken
string

Your Payabli API token.

environment
'sandbox' | 'production'Defaults to sandbox

The environment to use.

Methods

The class has the following methods:

create
(subscription: SubscriptionRequest) => Promise<any>

Creates a new subscription.

get
(subId: number) => Promise<any>

Fetches a subscription based on ID.

update
(subId: number, subscription: Partial<SubscriptionRequest>) => Promise<any>

Updates an existing subscription based on ID.

delete
(subId: number) => Promise<any>

Deletes a subscription based on ID.

list
() => Promise<any>

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 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
1type Environment = "sandbox" | "production";
2
3type PaymentMethodCard = {
4 method: "card";
5 cardnumber: string;
6 cardexp: string;
7 cardcvv?: string | null;
8 cardzip?: string | null;
9 cardHolder?: string | null;
10 initiator?: "payor" | "merchant";
11 saveIfSuccess?: boolean;
12}
13
14type PaymentMethodAch = {
15 method: "ach";
16 achAccount: string | null;
17 achRouting: string | null;
18 achHolder?: string | null;
19 achAccountType?: "Checking" | "Savings" | null;
20 achHolderType?: "personal" | "business" | null;
21 initiator?: "payor" | "merchant";
22 achCode: "PPD" | "TEL" | "WEB" | "CCD";
23 saveIfSuccess?: boolean;
24}
25
26type PaymentMethodStored = {
27 storedMethodId: string;
28 initiator?: "payor" | "merchant";
29 storedMethodUsageType?: "unscheduled" | "subscription" | "recurring";
30}
31
32type PaymentDetails = {
33 totalAmount: number;
34 serviceFee?: number | null;
35 currency?: string | null;
36 checkNumber?: string | null;
37 checkImage?: object | null;
38 categories?: {
39 label: string;
40 amount: number;
41 description?: string | null;
42 qty?: number | null;
43 }[] | null;
44 splitFunding?: {
45 recipientEntryPoint?: string | null;
46 accountId?: string | null;
47 description?: string | null;
48 amount?: number | null;
49 }[] | null;
50};
51
52type CustomerData = {
53 customerId?: number | null;
54 firstName?: string | null;
55 lastName?: string | null;
56 company?: string | null;
57 customerNumber?: string | null;
58 billingAddress1?: string | null;
59 billingAddress2?: string | null;
60 billingCity?: string | null;
61 billingState?: string | null;
62 billingZip?: string | null;
63 billingCountry?: string | null;
64 billingPhone?: string | null;
65 billingEmail?: string | null;
66 shippingAddress1?: string | null;
67 shippingAddress2?: string | null;
68 shippingCity?: string | null;
69 shippingState?: string | null;
70 shippingZip?: string | null;
71 shippingCountry?: string | null;
72 additionalData?: { [key: string]: any } | null;
73 identifierFields?: (string | null)[] | null;
74};
75
76type InvoiceData = {
77 invoiceNumber?: string | null; // Max length: 250
78 invoiceDate?: string | null; // Formats: YYYY-MM-DD, MM/DD/YYYY
79 invoiceDueDate?: string | null; // Formats: YYYY-MM-DD, MM/DD/YYYY
80 invoiceType?: 0 | null; // Only 0 is supported
81 invoiceEndDate?: string | null; // Formats: YYYY-MM-DD, MM/DD/YYYY
82 invoiceStatus?: 0 | 1 | 2 | 4 | 99 | null; // Status values
83 frequency?: "one-time" | "weekly" | "every2weeks" | "every6months" | "monthly" | "every3months" | "annually" | null;
84 paymentTerms?: "PIA" | "CIA" | "UR" | "NET10" | "NET20" | "NET30" | "NET45" | "NET60" | "NET90" | "EOM" | "MFI"
85 | "5MFI" | "10MFI" | "15MFI" | "20MFI" | "2/10NET30" | "UF" | "10UF" | "20UF" | "25UF" | "50UF" | null;
86 termsConditions?: string | null;
87 notes?: string | null;
88 tax?: number | null;
89 discount?: number | null;
90 invoiceAmount?: number | null;
91 freightAmount?: number | null;
92 dutyAmount?: number | null;
93 purchaseOrder?: string | null;
94 firstName?: string | null;
95 lastName?: string | null;
96 company?: string | null;
97 shippingAddress1?: string | null; // Max length: 250
98 shippingAddress2?: string | null; // Max length: 100
99 shippingCity?: string | null; // Max length: 250
100 shippingState?: string | null;
101 shippingZip?: string | null; // Max length: 50
102 shippingCountry?: string | null;
103 shippingEmail?: string | null; // Max length: 320
104 shippingPhone?: string | null;
105 shippingFromZip?: string | null;
106 summaryCommodityCode?: string | null;
107 items?: {
108 itemProductName: string | null; // Max length: 250
109 itemCost: number;
110 itemQty: number | null;
111 itemProductCode?: string | null; // Max length: 250
112 itemDescription?: string | null; // Max length: 250
113 itemCommodityCode?: string | null; // Max length: 250
114 itemUnitOfMeasure?: string | null; // Max length: 100
115 itemMode?: 0 | 1 | 2 | null;
116 itemCategories?: (string | null)[] | null;
117 itemTotalAmount?: number | null;
118 itemTaxAmount?: number | null;
119 itemTaxRate?: number | null;
120 }[] | null;
121 attachments?: object | null;
122 additionalData?: { [key: string]: any } | null;
123};
124
125type SubscriptionRequest = {
126 subdomain?: string | null;
127 source?: string | null;
128 setPause?: boolean;
129 paymentMethod: PaymentMethodCard | PaymentMethodAch | PaymentMethodStored;
130 paymentDetails: PaymentDetails;
131 customerData: CustomerData;
132 invoiceData?: InvoiceData;
133 scheduleDetails: {
134 planId: number;
135 startDate: string;
136 endDate?: string | null;
137 frequency: string;
138 };
139};
140
141type SubscriptionManagerConfig = {
142 entryPoint: string;
143 apiToken: string;
144 environment?: Environment;
145};
146
147class SubscriptionManager {
148 private baseUrl: string;
149 private apiToken: string;
150 private entryPoint: string;
151
152 constructor({ entryPoint, apiToken, environment = "sandbox" }: SubscriptionManagerConfig) {
153 this.baseUrl = this.getBaseUrl(environment);
154 this.apiToken = apiToken;
155 this.entryPoint = entryPoint;
156 }
157
158 private getBaseUrl(env: Environment): string {
159 switch (env) {
160 case "production":
161 return "https://api.payabli.com/api";
162 case "sandbox":
163 default:
164 return "https://api-sandbox.payabli.com/api";
165 }
166 }
167
168 async create(subscription: SubscriptionRequest): Promise<any> {
169 try {
170 const response = await fetch(`${this.baseUrl}/Subscription/add`, {
171 method: "POST",
172 headers: {
173 "Content-Type": "application/json",
174 "requestToken": this.apiToken,
175 },
176 body: JSON.stringify({ ...subscription, entryPoint: this.entryPoint }),
177 });
178
179 if (!response.ok) {
180 const errorData = await response.json();
181 console.error("Error response:", errorData);
182 throw new Error(errorData.message || "Failed to create subscription");
183 }
184
185 const res = await response.json();
186 return res;
187 } catch (error: any) {
188 console.error("Error creating subscription:", error.message);
189 throw error;
190 }
191 }
192
193 async update(subId: number, subscription: Partial<SubscriptionRequest>): Promise<any> {
194 try {
195 const response = await fetch(`${this.baseUrl}/Subscription/${subId}`, {
196 method: "PUT",
197 headers: {
198 "Content-Type": "application/json",
199 "requestToken": this.apiToken,
200 },
201 body: JSON.stringify({ ...subscription, entryPoint: this.entryPoint }),
202 });
203
204 if (!response.ok) {
205 const errorData = await response.json();
206 console.error("Error response:", errorData);
207 throw new Error(errorData.message || "Failed to update subscription");
208 }
209
210 const res = await response.json();
211 return res;
212 } catch (error: any) {
213 console.error("Error updating subscription:", error.message);
214 throw error;
215 }
216 }
217
218 async get(subId: number): Promise<any> {
219 try {
220 const response = await fetch(`${this.baseUrl}/Subscription/${subId}`, {
221 method: "GET",
222 headers: {
223 "Content-Type": "application/json",
224 "requestToken": this.apiToken,
225 },
226 });
227
228 if (!response.ok) {
229 const errorData = await response.json();
230 console.error("Error response:", errorData);
231 throw new Error(errorData.message || "Failed to get subscription");
232 }
233
234 const res = await response.json();
235 return res;
236 } catch (error: any) {
237 console.error("Error getting subscription:", error.message);
238 throw error;
239 }
240 }
241
242 async list(): Promise<any> {
243 try {
244 const response = await fetch(`${this.baseUrl}/Query/subscriptions/${this.entryPoint}`, {
245 method: "GET",
246 headers: {
247 "Content-Type": "application/json",
248 "requestToken": this.apiToken,
249 },
250 });
251
252 if (!response.ok) {
253 const errorData = await response.json();
254 console.error("Error response:", errorData);
255 throw new Error(errorData.message || "Failed to get list of subscriptions");
256 }
257
258 const res = await response.json();
259 return res;
260 } catch (error: any) {
261 console.error("Error getting list of subscriptions:", error.message);
262 throw error;
263 }
264 }
265
266 async delete(subId: number): Promise<any> {
267 try {
268 const response = await fetch(`${this.baseUrl}/Subscription/${subId}`, {
269 method: "DELETE",
270 headers: {
271 "Content-Type": "application/json",
272 "requestToken": this.apiToken,
273 },
274 });
275
276 if (!response.ok) {
277 const errorData = await response.json();
278 console.error("Error response:", errorData);
279 throw new Error(errorData.message || "Failed to delete subscription");
280 }
281
282 const res = await response.json();
283 return res;
284 } catch (error: any) {
285 console.error("Error deleting subscription:", error.message);
286 throw error;
287 }
288 }
289}
290
291export 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
1const paysub = new SubscriptionManager({
2 entryPoint: "A123456789", // replace with your entrypoint
3 apiToken: "o.Se...RnU=", // replace with your API key
4 environment: "sandbox"
5});
6
7const sub1 = await paysub.create({
8 paymentMethod: {
9 method: "card",
10 initiator: "payor",
11 cardHolder: "John Cassian",
12 cardzip: "12345",
13 cardcvv: "996",
14 cardexp: "12/34",
15 cardnumber: "6011000993026909",
16 },
17 paymentDetails: {
18 totalAmount: 100,
19 serviceFee: 0,
20 },
21 customerData: {
22 customerId: 4440,
23 },
24 scheduleDetails: {
25 planId: 1,
26 startDate: "05-20-2025",
27 endDate: "05-20-2026",
28 frequency: "weekly"
29 }
30})
31
32const sub1Id = sub1.responseData; // hold the subscription ID returned from the API
33
34console.log(sub1Id); // log the response from the API after creating the subscription
35
36console.log(await paysub.get(sub1Id)); // log the details of the created subscription
37
38await paysub.update(sub1Id, {
39 paymentDetails: {
40 totalAmount: 150, // update the total amount
41 },
42});
43
44console.log(sub1Id); // log the response from the API after updating the subscription
45
46console.log(await paysub.get(sub1Id)); // log the details of the updated subscription
47
48console.log(await paysub.delete(sub1Id)); // log the response from the API after deleting the subscription
49
50const subList = await paysub.list() // fetch the list of all subscriptions
51
52console.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 explained in surrounding text
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.

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.

1const url = "https://api-sandbox.payabli.com/api/Notification";
2const headers = {
3 "requestToken": "o.Se...RnU=", // Replace with your API key
4 "Content-Type": "application/json"
5};
6
7// Base payload structure
8const basePayload = {
9 content: {
10 timeZone: "-5",
11 webHeaderParameters: [
12 // Replace with your own authentication parameters
13 { key: "myAuthorizationID", value: "1234" }
14 ],
15 eventType: "DeclinedPayment",
16 },
17 method: "web",
18 frequency: "untilcancelled",
19 target: "https://my-app-url.com/", // Replace with your own URL
20 status: 1,
21 ownerType: 2,
22 ownerId: "255" // Replace with your own paypoint ID
23};
24
25// Function to send webhooks
26const sendWebhook = async () => {
27 const payload = basePayload;
28
29 try {
30 const response = await fetch(url, {
31 method: "POST",
32 headers,
33 body: JSON.stringify(payload)
34 });
35
36 const responseText = await response.text();
37 console.log(`Notification for DeclinedPayment, Status: ${response.status}, Response: ${responseText}`);
38 } catch (error) {
39 console.error(`Failed to create notification for DeclinedPayment:`, error);
40 }
41};
42
43sendWebhook();

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
1// npm install express
2// npm install --save-dev @types/express
3import express, { Request, Response } from "express";
4
5// Constants for API request
6const ENVIRONMENT: "sandbox" | "production" = "sandbox"; // Change as needed
7const ENTRY = "your-entry"; // Replace with actual entrypoint value
8const API_KEY = "your-api-key"; // Replace with actual API key
9
10// API base URLs based on environment
11const API_BASE_URLS = {
12 sandbox: "https://api-sandbox.payabli.com",
13 production: "https://api.payabli.com",
14};
15
16// Define the expected webhook payload structure
17interface WebhookPayload {
18 Event?: string;
19 transId?: string;
20 [key: string]: any; // Allow additional properties
21}
22
23// Function to handle declined payments
24const handleDeclinedPayment = async (transId?: string): Promise<void> => {
25 if (!transId) {
26 console.log("DeclinedPayment received, but it didn't include a transaction ID.");
27 return Promise.resolve();
28 }
29
30 // Fetch transaction from transId in DeclinedPayment event
31 const transactionQueryUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Query/transactions/${ENTRY}?transId(eq)=${transId}`;
32 const headers = { requestToken: API_KEY };
33
34 // Get subscription ID from transaction
35 return fetch(transactionQueryUrl, { method: "GET", headers })
36 .then(res => res.ok ? res.json() : Promise.reject(`HTTP ${res.status}: ${res.statusText}`))
37 .then(data => {
38 const subscriptionId = data?.Records[0]?.ScheduleReference;
39 if (!subscriptionId) {
40 console.log("DeclinedPayment notification received, but no subscription ID found.");
41 return;
42 }
43 return subscriptionRetry(subscriptionId); // Perform logic on subscription with subscription ID
44 })
45 .catch(error => console.error(`Error handling declined payment: ${error}`));
46};
47
48const subscriptionRetry = async (subId: string): Promise<void> => {
49 const subscriptionUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Subscription/${subId}`;
50 const headers = {
51 "Content-Type": "application/json",
52 requestToken: API_KEY,
53 };
54
55 const body = JSON.stringify({
56 setPause: false, // unpause subscription after decline
57 paymentDetails: {
58 storedMethodId: "4000e8c6-...-1323", // Replace with actual stored method ID
59 storedMethodUsageType: "recurring",
60 },
61 scheduleDetails: {
62 startDate: "2025-05-20", // Must be a future date
63 },
64 });
65
66 return fetch(subscriptionUrl, { method: "PUT", headers, body })
67 .then(response =>
68 !response.ok
69 ? Promise.reject(`HTTP ${response.status}: ${response.statusText}`)
70 : response.json())
71 .then(data => console.log("Subscription updated successfully:", data))
72 .catch(error => console.error("Error updating subscription:", error));
73};
74
75const app = express();
76const PORT = 3333;
77
78// Middleware to parse JSON payloads
79app.use(express.json());
80
81// Webhook endpoint
82app.post("/webhook", (req: Request, res: Response): void => {
83 const payload: WebhookPayload = req.body;
84
85 if (payload.Event === "DeclinedPayment") {
86 handleDeclinedPayment(payload.transId);
87 }
88
89 res.sendStatus(200); // Acknowledge receipt
90});
91
92// Start server
93app.listen(PORT, () => {
94 console.log(`Server is running on port ${PORT}, Environment: ${ENVIRONMENT}`);
95});

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.

TS
1// use in a Next.js project
2// something like /pages/api/webhook-payabli.ts
3import { NextApiRequest, NextApiResponse } from "next";
4
5// Constants for API request
6const ENVIRONMENT: "sandbox" | "production" = "sandbox"; // Change as needed
7const ENTRY = "your-entry"; // Replace with actual entrypoint value
8const API_KEY = "your-api-key"; // Replace with actual API key
9
10// API base URLs based on environment
11const API_BASE_URLS = {
12 sandbox: "https://api-sandbox.payabli.com",
13 production: "https://api.payabli.com",
14};
15
16// Define the expected webhook payload structure
17interface WebhookPayload {
18 Event?: string;
19 transId?: string;
20 [key: string]: any; // Allow additional properties
21}
22
23// Function to handle declined payments
24const handleDeclinedPayment = async (transId?: string): Promise<void> => {
25 if (!transId) {
26 console.log("DeclinedPayment notification received, but it didn't include a transaction ID.");
27 return Promise.resolve();
28 }
29
30 // Fetch transaction from transId in DeclinedPayment event
31 const transactionQueryUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Query/transactions/${ENTRY}?transId(eq)=${transId}`;
32 const headers = { requestToken: API_KEY };
33
34 // Get subscription ID from transaction
35 return fetch(transactionQueryUrl, { method: "GET", headers })
36 .then(res => res.ok ? res.json() : Promise.reject(`HTTP ${res.status}: ${res.statusText}`))
37 .then(data => {
38 const subscriptionId = data?.Records[0]?.ScheduleReference;
39 if (!subscriptionId) {
40 console.log("DeclinedPayment notification received, but no subscription ID found.");
41 return;
42 }
43 return subscriptionRetry(subscriptionId); // Perform logic on subscription with subscription ID
44 })
45 .catch(error => console.error(`Error handling declined payment: ${error}`));
46};
47
48const subscriptionRetry = async (subId: string): Promise<void> => {
49 const subscriptionUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Subscription/${subId}`;
50 const headers = {
51 "Content-Type": "application/json",
52 requestToken: API_KEY,
53 };
54
55 const body = JSON.stringify({
56 setPause: false, // unpause subscription after decline
57 paymentDetails: {
58 storedMethodId: "4000e8c6-...-1323", // Replace with actual stored method ID
59 storedMethodUsageType: "recurring",
60 },
61 scheduleDetails: {
62 startDate: "2025-05-20", // Must be a future date
63 },
64 });
65
66 return fetch(subscriptionUrl, { method: "PUT", headers, body })
67 .then(response =>
68 !response.ok
69 ? Promise.reject(`HTTP ${response.status}: ${response.statusText}`)
70 : response.json())
71 .then(data => console.log("Subscription updated successfully:", data))
72 .catch(error => console.error("Error updating subscription:", error));
73};
74
75export default (req: NextApiRequest, res: NextApiResponse): void => {
76 if (req.method === "POST") {
77 const payload: WebhookPayload = req.body;
78
79 if (payload.Event === "DeclinedPayment") {
80 handleDeclinedPayment(payload.transId);
81 }
82
83 res.status(200).end(); // Acknowledge receipt
84 } else {
85 res.setHeader("Allow", ["POST"]);
86 res.status(405).end(`Method ${req.method} Not Allowed`);
87 }
88};