Subscription utility code

Use example code to enhance your subscription management via the API
View as MarkdownOpen in Claude
Applies to:Developers

This guide contains code snippets to help you manage subscriptions in your apps using Payabli’s server SDKs. To learn more about Payabli’s server SDKs, see the Server SDKs overview page.

The guide includes:

  • A list of SDK methods for subscription management with links to the reference documentation for each method.
  • A set of examples showing how to implement retry logic for declined subscription payments.

SDK subscription methods

This section covers API endpoints for subscription management and their corresponding methods in Payabli’s server SDKs. See the SDK reference documentation in each endpoint for more details on the request and response formats.

Create subscription

The POST /Subscription/add endpoint creates a new subscription for a paypoint. See Create subscription for the full API reference. The following table lists the methods in each SDK that correspond to this endpoint, along with links to the reference documentation for each method:

SDKMethodReference
TypeScriptsubscription.newSubscription()reference.md
Pythonsubscription.new_subscription()reference.md
C#subscription.NewSubscriptionAsync()reference.md
Gosubscription.NewSubscription()reference.md
PHPsubscription->newSubscription()reference.md
Javasubscription.newSubscription()reference.md
Rubysubscription.new_subscription()reference.md
Rustsubscription.new_subscription()reference.md

This snippet shows how to create a subscription using the POST /Subscription/add endpoint. Select a programming language in the dropdown to see the code example for that language.

POST
/api/Subscription/add
1curl -X POST https://api-sandbox.payabli.com/api/Subscription/add \
2 -H "Content-Type: application/json" \
3 -d '{
4 "customerData": {
5 "customerId": 4440
6 },
7 "entryPoint": "8cfec329267",
8 "paymentDetails": {
9 "totalAmount": 100,
10 "serviceFee": 0
11 },
12 "paymentMethod": {
13 "initiator": "merchant",
14 "storedMethodId": "1ec55af9-7b5a-4ff0-81ed-c12d2f95e135-4440",
15 "storedMethodUsageType": "recurring"
16 },
17 "scheduleDetails": {
18 "endDate": "2025-03-20",
19 "frequency": "weekly",
20 "planId": 1,
21 "startDate": "2024-09-20"
22 }
23}'

This snippet shows the response from the POST /Subscription/add endpoint.

Response
1{
2 "responseText": "Success",
3 "responseData": 396,
4 "customerId": 4440,
5 "isSuccess": true
6}

Get subscription

The GET /Subscription/{subId} endpoint retrieves details for a subscription. See Get subscription for the full API reference. The following table lists the methods in each SDK that correspond to this endpoint, along with links to the reference documentation for each method:

SDKMethodReference
TypeScriptsubscription.getSubscription()reference.md
Pythonsubscription.get_subscription()reference.md
C#subscription.GetSubscriptionAsync()reference.md
Gosubscription.GetSubscription()reference.md
PHPsubscription->getSubscription()reference.md
Javasubscription.getSubscription()reference.md
Rubysubscription.get_subscription()reference.md
Rustsubscription.get_subscription()reference.md

This snippet shows how to get a subscription using the GET /Subscription/{subId} endpoint. Select a programming language in the dropdown to see the code example for that language.

GET
/api/Subscription/:subId
1curl https://api-sandbox.payabli.com/api/Subscription/231

This snippet shows the response from the GET /Subscription/{subId} endpoint.

Response
1{
2 "EndDate": "2025-10-19T00:00:00Z",
3 "LastRun": "2025-10-19T00:00:00Z",
4 "NextDate": "2025-10-19T00:00:00Z",
5 "StartDate": "2025-10-19T00:00:00Z",
6 "CreatedAt": "2022-07-01T15:00:01Z",
7 "Customer": {
8 "Identifiers": [
9 "\\\"firstname\\\"",
10 "\\\"lastname\\\"",
11 "\\\"email\\\"",
12 "\\\"customId\\\""
13 ],
14 "FirstName": "John",
15 "LastName": "Doe",
16 "CompanyName": "Sunshine LLC",
17 "BillingAddress1": "1111 West 1st Street",
18 "BillingAddress2": "Suite 200",
19 "BillingCity": "Miami",
20 "BillingState": "FL",
21 "BillingZip": "45567",
22 "BillingCountry": "US",
23 "BillingPhone": "5555555555",
24 "BillingEmail": "example@email.com",
25 "CustomerNumber": "C-90010",
26 "ShippingAddress1": "123 Walnut St",
27 "ShippingAddress2": "STE 900",
28 "ShippingCity": "Johnson City",
29 "ShippingState": "TN",
30 "ShippingZip": "37619",
31 "ShippingCountry": "US",
32 "customerId": 4440,
33 "customerStatus": 1,
34 "AdditionalData": null
35 },
36 "EntrypageId": 0,
37 "ExternalPaypointID": "Paypoint-100",
38 "FeeAmount": 3,
39 "Frequency": "monthly",
40 "IdSub": 396,
41 "invoiceData": {
42 "AdditionalData": null,
43 "attachments": [
44 {}
45 ],
46 "company": "ACME, INC",
47 "discount": 10,
48 "dutyAmount": 0,
49 "firstName": "Chad",
50 "freightAmount": 10,
51 "frequency": "onetime",
52 "invoiceAmount": 105,
53 "invoiceDate": "2025-07-01",
54 "invoiceDueDate": "2025-07-01",
55 "invoiceEndDate": "2025-07-01",
56 "invoiceNumber": "INV-2345",
57 "invoiceStatus": 1,
58 "invoiceType": 0,
59 "items": [
60 {
61 "itemCost": 5,
62 "itemProductName": "Materials deposit",
63 "itemQty": 1
64 }
65 ],
66 "lastName": "Mercia",
67 "notes": "Example notes.",
68 "paymentTerms": "PIA",
69 "purchaseOrder": "PO-345",
70 "shippingAddress1": "123 Walnut St",
71 "shippingAddress2": "STE 900",
72 "shippingCity": "Johnson City",
73 "shippingCountry": "US",
74 "shippingEmail": "example@email.com",
75 "shippingFromZip": "30040",
76 "shippingPhone": "5555555555",
77 "shippingState": "TN",
78 "shippingZip": "37619",
79 "summaryCommodityCode": "501718",
80 "tax": 2.05,
81 "termsConditions": "Must be paid before work scheduled."
82 },
83 "LastUpdated": "2022-07-01T15:00:01Z",
84 "LeftCycles": 15,
85 "Method": "card",
86 "NetAmount": 3762.87,
87 "ParentOrgName": "PropertyManager Pro",
88 "PaymentData": {
89 "AccountExp": "11/29",
90 "accountId": "accountId",
91 "AccountType": "visa",
92 "AccountZip": "90210",
93 "binData": {
94 "binMatchedLength": "6",
95 "binCardBrand": "Visa",
96 "binCardType": "Credit",
97 "binCardCategory": "PLATINUM",
98 "binCardIssuer": "Bank of Example",
99 "binCardIssuerCountry": "United States",
100 "binCardIssuerCountryCodeA2": "US",
101 "binCardIssuerCountryNumber": "840",
102 "binCardIsRegulated": "false",
103 "binCardUseCategory": "Consumer",
104 "binCardIssuerCountryCodeA3": "USA"
105 },
106 "HolderName": "Chad Mercia",
107 "Initiator": "payor",
108 "MaskedAccount": "4XXXXXXXX1111",
109 "orderDescription": "Depost for materials for 123 Walnut St",
110 "paymentDetails": {
111 "totalAmount": 100,
112 "categories": [
113 {
114 "amount": 1000,
115 "label": "Deposit"
116 }
117 ],
118 "checkImage": {
119 "key": "value"
120 },
121 "checkNumber": "107",
122 "currency": "USD",
123 "serviceFee": 0,
124 "splitFunding": [
125 {}
126 ]
127 },
128 "Sequence": "subsequent",
129 "SignatureData": "SignatureData",
130 "StoredId": "1ec55af9-7b5a-4ff0-81ed-c12d2f95e135-4440",
131 "StoredMethodUsageType": "subscription"
132 },
133 "PaypointDbaname": "Sunshine Gutters",
134 "PaypointEntryname": "d193cf9a46",
135 "PaypointId": 3040,
136 "PaypointLegalname": "Sunshine Services, LLC",
137 "PlanId": 0,
138 "Source": "api",
139 "SubEvents": [
140 {
141 "description": "TransferCreated",
142 "eventTime": "2023-07-05T22:31:06Z",
143 "extraData": null,
144 "refData": "refData",
145 "source": "api"
146 }
147 ],
148 "SubStatus": 1,
149 "SubscriptionType": "Regular",
150 "TotalAmount": 103,
151 "TotalCycles": 24,
152 "UntilCancelled": true
153}

Update subscription

The PUT /Subscription/{subId} endpoint updates an existing subscription. See Update subscription for the full API reference. The following table lists the methods in each SDK that correspond to this endpoint, along with links to the reference documentation for each method:

SDKMethodReference
TypeScriptsubscription.updateSubscription()reference.md
Pythonsubscription.update_subscription()reference.md
C#subscription.UpdateSubscriptionAsync()reference.md
Gosubscription.UpdateSubscription()reference.md
PHPsubscription->updateSubscription()reference.md
Javasubscription.updateSubscription()reference.md
Rubysubscription.update_subscription()reference.md
Rustsubscription.update_subscription()reference.md

This snippet shows how to update a subscription using the PUT /Subscription/{subId} endpoint. Select a programming language in the dropdown to see the code example for that language.

PUT
/api/Subscription/:subId
1curl -X PUT https://api-sandbox.payabli.com/api/Subscription/231 \
2 -H "Content-Type: application/json" \
3 -d '{
4 "setPause": true
5}'

This snippet shows the response from the PUT /Subscription/{subId} endpoint.

Response
1{
2 "responseText": "Success",
3 "isSuccess": true,
4 "responseData": "396 paused",
5 "customerId": 4440
6}

Delete subscription

The DELETE /Subscription/{subId} endpoint deletes a subscription and prevents future charges. See Delete subscription for the full API reference. The following table lists the methods in each SDK that correspond to this endpoint, along with links to the reference documentation for each method:

SDKMethodReference
TypeScriptsubscription.removeSubscription()reference.md
Pythonsubscription.remove_subscription()reference.md
C#subscription.RemoveSubscriptionAsync()reference.md
Gosubscription.RemoveSubscription()reference.md
PHPsubscription->removeSubscription()reference.md
Javasubscription.removeSubscription()reference.md
Rubysubscription.remove_subscription()reference.md
Rustsubscription.remove_subscription()reference.md

This snippet shows how to delete a subscription using the DELETE /Subscription/{subId} endpoint. Select a programming language in the dropdown to see the code example for that language.

DELETE
/api/Subscription/:subId
1curl -X DELETE https://api-sandbox.payabli.com/api/Subscription/231

This snippet shows the response from the DELETE /Subscription/{subId} endpoint.

Response
1{
2 "responseText": "Success",
3 "isSuccess": true,
4 "responseData": "396"
5}

List subscriptions by paypoint

The GET /Query/subscriptions/{entry} endpoint returns all subscriptions for a paypoint. See List subscriptions by paypoint for the full API reference. The following table lists the methods in each SDK that correspond to this endpoint, along with links to the reference documentation for each method:

SDKMethodReference
TypeScriptquery.listSubscriptions()reference.md
Pythonquery.list_subscriptions()reference.md
C#query.ListSubscriptionsAsync()reference.md
Goquery.ListSubscriptions()reference.md
PHPquery->listSubscriptions()reference.md
Javaquery.listSubscriptions()reference.md
Rubyquery.list_subscriptions()reference.md
Rustquery.list_subscriptions()reference.md

This snippet shows how to list subscriptions by paypoint using the GET /Query/subscriptions/{entry} endpoint. Select a programming language in the dropdown to see the code example for that language.

GET
/api/Query/subscriptions/:entry
1curl -G https://api-sandbox.payabli.com/api/Query/subscriptions/8cfec329267 \
2 -d fromRecord=251 \
3 -d limitRecord=0 \
4 -d sortBy=desc(field_name)

This snippet shows the response from the GET /Query/subscriptions/{entry} endpoint.

Response
1{
2 "Records": [
3 {
4 "EndDate": "2026-03-20T00:00:00Z",
5 "LastRun": "2024-01-02T14:32:11Z",
6 "NextDate": "2024-07-20T00:00:00Z",
7 "StartDate": "2024-07-20T00:00:00Z",
8 "StoredMethod": {
9 "IdPmethod": "6edcbb56-9c0e-4003-b3d1-99abf149ba0e",
10 "Method": "card",
11 "Descriptor": "Visa for subscriptions",
12 "MaskedAccount": "4XXXXXXX0010",
13 "ExpDate": "0924",
14 "HolderName": "Lisandra Smitch",
15 "AchSecCode": null,
16 "AchHolderType": null,
17 "IsValidatedACH": false,
18 "BIN": "",
19 "binData": {
20 "binMatchedLength": "6",
21 "binCardBrand": "Visa",
22 "binCardType": "Credit",
23 "binCardCategory": "PLATINUM",
24 "binCardIssuer": "Bank of Example",
25 "binCardIssuerCountry": "United States",
26 "binCardIssuerCountryCodeA2": "US",
27 "binCardIssuerCountryNumber": "840",
28 "binCardIsRegulated": "false",
29 "binCardUseCategory": "Consumer",
30 "binCardIssuerCountryCodeA3": "USA"
31 },
32 "ABA": "",
33 "PostalCode": "37612",
34 "MethodType": "Single Merchant",
35 "LastUpdated": "2023-12-14T08:51:10Z",
36 "CardUpdatedOn": null
37 },
38 "CreatedAt": "2023-12-14T08:51:10Z",
39 "Customer": {
40 "Identifiers": [
41 "\\\"firstname\\\"",
42 "\\\"lastname\\\"",
43 "\\\"email\\\""
44 ],
45 "FirstName": "Lisandra",
46 "LastName": "Smitch",
47 "CompanyName": "Sunshine LLC",
48 "BillingAddress1": "68 Golden Drive",
49 "BillingAddress2": "",
50 "BillingCity": "Johnson City",
51 "BillingState": "TN",
52 "BillingZip": "37612",
53 "BillingCountry": "US",
54 "BillingPhone": "",
55 "BillingEmail": "company@payabli.com",
56 "CustomerNumber": "C-90010",
57 "ShippingAddress1": "68 Golden Drive",
58 "ShippingCity": "Johnson City",
59 "ShippingState": "TN",
60 "ShippingZip": "37612",
61 "ShippingCountry": "US",
62 "customerId": 4440,
63 "customerStatus": 1,
64 "AdditionalData": null
65 },
66 "EntrypageId": 0,
67 "ExternalPaypointID": "f743aed24a-10",
68 "FeeAmount": 0,
69 "Frequency": "monthly",
70 "IdSub": 580,
71 "invoiceData": {
72 "AdditionalData": null,
73 "frequency": "onetime",
74 "invoiceAmount": 100,
75 "invoiceNumber": "INV-2345",
76 "invoiceStatus": 1,
77 "invoiceType": 1,
78 "items": [
79 {
80 "itemCost": 10,
81 "itemDescription": "service",
82 "itemMode": 1,
83 "itemProductName": "Mat replacement",
84 "itemQty": 5,
85 "itemTotalAmount": 50
86 },
87 {
88 "itemCost": 5,
89 "itemDescription": "service",
90 "itemMode": 1,
91 "itemProductName": "Mat clean",
92 "itemQty": 10,
93 "itemTotalAmount": 50
94 }
95 ]
96 },
97 "LastUpdated": "2023-12-14T08:51:10Z",
98 "LeftCycles": 20,
99 "Method": "card",
100 "NetAmount": 10,
101 "ParentOrgName": "FitnessManager",
102 "PaymentData": {
103 "AccountExp": "0924",
104 "AccountType": "unknown",
105 "AccountZip": "37612",
106 "binData": {
107 "binMatchedLength": "6",
108 "binCardBrand": "Visa",
109 "binCardType": "Credit",
110 "binCardCategory": "PLATINUM",
111 "binCardIssuer": "Bank of Example",
112 "binCardIssuerCountry": "United States",
113 "binCardIssuerCountryCodeA2": "US",
114 "binCardIssuerCountryNumber": "840",
115 "binCardIsRegulated": "false",
116 "binCardUseCategory": "Consumer",
117 "binCardIssuerCountryCodeA3": "USA"
118 },
119 "HolderName": "Lisandra Smitch",
120 "Initiator": "payor",
121 "MaskedAccount": "2222 4XXXXXX0010",
122 "paymentDetails": {
123 "totalAmount": 100,
124 "currency": "USD",
125 "serviceFee": 0
126 },
127 "Sequence": "subsequent",
128 "StoredMethodUsageType": "subscription"
129 },
130 "PaypointDbaname": "Athlete Factory LLC",
131 "PaypointEntryname": "473ac58b0",
132 "PaypointId": 3040,
133 "PaypointLegalname": "Athlete Factory LLC",
134 "PlanId": 1,
135 "SubEvents": [
136 {
137 "description": "created",
138 "eventTime": "2023-12-14T13:51:10Z",
139 "refData": "00-3470dfe2658b492811630255602f3fb5-d06fe0f72110000-00"
140 },
141 {
142 "description": "updated",
143 "eventTime": "2023-12-15T10:30:00Z",
144 "refData": "01-1234abcde6789fghij4567klmnopqr89-abcdefghi12345678-01",
145 "source": "web app"
146 }
147 ],
148 "SubStatus": 1,
149 "SubscriptionType": "Regular",
150 "TotalAmount": 100,
151 "TotalCycles": 20,
152 "UntilCancelled": false
153 }
154 ],
155 "Summary": {
156 "pageIdentifier": "XXXXXXXXXXXXXXXXXXX",
157 "pageSize": 20,
158 "totalAmount": 150.22,
159 "totalNetAmount": 150.22,
160 "totalPages": 1,
161 "totalRecords": 2
162 }
163}

List subscriptions by organization

The GET /Query/subscriptions/org/{orgId} endpoint returns all subscriptions across for an organization. See List subscriptions by organization for the full API reference. The following table lists the methods in each SDK that correspond to this endpoint, along with links to the reference documentation for each method:

SDKMethodReference
TypeScriptquery.listSubscriptionsOrg()reference.md
Pythonquery.list_subscriptions_org()reference.md
C#query.ListSubscriptionsOrgAsync()reference.md
Goquery.ListSubscriptionsOrg()reference.md
PHPquery->listSubscriptionsOrg()reference.md
Javaquery.listSubscriptionsOrg()reference.md
Rubyquery.list_subscriptions_org()reference.md
Rustquery.list_subscriptions_org()reference.md

This snippet shows how to list subscriptions by organization using the GET /Query/subscriptions/org/{orgId} endpoint. Select a programming language in the dropdown to see the code example for that language.

GET
/api/Query/subscriptions/org/:orgId
1curl -G https://api-sandbox.payabli.com/api/Query/subscriptions/org/123 \
2 -d fromRecord=251 \
3 -d limitRecord=0 \
4 -d sortBy=desc(field_name)

This snippet shows the response from the GET /Query/subscriptions/org/{orgId} endpoint.

Response
1{
2 "Records": [
3 {
4 "EndDate": "2026-03-20T00:00:00Z",
5 "LastRun": "2024-01-02T14:32:11Z",
6 "NextDate": "2024-07-20T00:00:00Z",
7 "StartDate": "2024-07-20T00:00:00Z",
8 "StoredMethod": {
9 "IdPmethod": "6edcbb56-9c0e-4003-b3d1-99abf149ba0e",
10 "Method": "card",
11 "Descriptor": "Visa for subscriptions",
12 "MaskedAccount": "4XXXXXXX0010",
13 "ExpDate": "0924",
14 "HolderName": "Lisandra Smitch",
15 "AchSecCode": null,
16 "AchHolderType": null,
17 "IsValidatedACH": false,
18 "BIN": "",
19 "binData": {
20 "binMatchedLength": "6",
21 "binCardBrand": "Visa",
22 "binCardType": "Credit",
23 "binCardCategory": "PLATINUM",
24 "binCardIssuer": "Bank of Example",
25 "binCardIssuerCountry": "United States",
26 "binCardIssuerCountryCodeA2": "US",
27 "binCardIssuerCountryNumber": "840",
28 "binCardIsRegulated": "false",
29 "binCardUseCategory": "Consumer",
30 "binCardIssuerCountryCodeA3": "USA"
31 },
32 "ABA": "",
33 "PostalCode": "37612",
34 "MethodType": "Single Merchant",
35 "LastUpdated": "2023-12-14T08:51:10Z",
36 "CardUpdatedOn": null
37 },
38 "CreatedAt": "2023-12-14T08:51:10Z",
39 "Customer": {
40 "Identifiers": [
41 "\\\"firstname\\\"",
42 "\\\"lastname\\\"",
43 "\\\"email\\\""
44 ],
45 "FirstName": "Lisandra",
46 "LastName": "Smitch",
47 "CompanyName": "Sunshine LLC",
48 "BillingAddress1": "68 Golden Drive",
49 "BillingAddress2": "",
50 "BillingCity": "Johnson City",
51 "BillingState": "TN",
52 "BillingZip": "37612",
53 "BillingCountry": "US",
54 "BillingPhone": "",
55 "BillingEmail": "company@payabli.com",
56 "CustomerNumber": "C-90010",
57 "ShippingAddress1": "68 Golden Drive",
58 "ShippingCity": "Johnson City",
59 "ShippingState": "TN",
60 "ShippingZip": "37612",
61 "ShippingCountry": "US",
62 "customerId": 4440,
63 "customerStatus": 1,
64 "AdditionalData": null
65 },
66 "EntrypageId": 0,
67 "ExternalPaypointID": "f743aed24a-10",
68 "FeeAmount": 0,
69 "Frequency": "monthly",
70 "IdSub": 580,
71 "invoiceData": {
72 "AdditionalData": null,
73 "frequency": "onetime",
74 "invoiceAmount": 100,
75 "invoiceNumber": "INV-2345",
76 "invoiceStatus": 1,
77 "invoiceType": 1,
78 "items": [
79 {
80 "itemCost": 10,
81 "itemDescription": "service",
82 "itemMode": 1,
83 "itemProductName": "Mat replacement",
84 "itemQty": 5,
85 "itemTotalAmount": 50
86 },
87 {
88 "itemCost": 5,
89 "itemDescription": "service",
90 "itemMode": 1,
91 "itemProductName": "Mat clean",
92 "itemQty": 10,
93 "itemTotalAmount": 50
94 }
95 ]
96 },
97 "LastUpdated": "2023-12-14T08:51:10Z",
98 "LeftCycles": 20,
99 "Method": "card",
100 "NetAmount": 10,
101 "ParentOrgName": "FitnessManager",
102 "PaymentData": {
103 "AccountExp": "0924",
104 "AccountType": "unknow",
105 "AccountZip": "37612",
106 "binData": {
107 "binMatchedLength": "6",
108 "binCardBrand": "Visa",
109 "binCardType": "Credit",
110 "binCardCategory": "PLATINUM",
111 "binCardIssuer": "Bank of Example",
112 "binCardIssuerCountry": "United States",
113 "binCardIssuerCountryCodeA2": "US",
114 "binCardIssuerCountryNumber": "840",
115 "binCardIsRegulated": "false",
116 "binCardUseCategory": "Consumer",
117 "binCardIssuerCountryCodeA3": "USA"
118 },
119 "HolderName": "Lisandra Smitch",
120 "Initiator": "payor",
121 "MaskedAccount": "2222 4XXXXXX0010",
122 "paymentDetails": {
123 "totalAmount": 100,
124 "currency": "USD",
125 "serviceFee": 0
126 },
127 "Sequence": "subsequent",
128 "StoredMethodUsageType": "subscription"
129 },
130 "PaypointDbaname": "Athlete Factory LLC",
131 "PaypointEntryname": "473ac58b0",
132 "PaypointId": 3040,
133 "PaypointLegalname": "Athlete Factory LLC",
134 "PlanId": 1,
135 "SubEvents": [
136 {
137 "description": "created",
138 "eventTime": "2023-12-14T13:51:10Z",
139 "refData": "00-3470dfe2658b492811630255602f3fb5-d06fe0f72110000-00"
140 },
141 {
142 "description": "updated",
143 "eventTime": "2023-12-15T10:30:00Z",
144 "refData": "01-1234abcde6789fghij4567klmnopqr89-abcdefghi12345678-01",
145 "source": "web app"
146 }
147 ],
148 "SubStatus": 1,
149 "SubscriptionType": "Regular",
150 "TotalAmount": 100,
151 "TotalCycles": 20,
152 "UntilCancelled": false
153 }
154 ],
155 "Summary": {
156 "pageIdentifier": "XXXXXXXXXXXXXXXXXXX",
157 "pageSize": 20,
158 "totalAmount": 150.22,
159 "totalNetAmount": 150.22,
160 "totalPages": 1,
161 "totalRecords": 2
162 }
163}

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:

Subscription retry flow from webhook to subscription update

Diagram: Subscription Retry Flow Process

This sequence diagram shows how to handle declined subscription payments:

  1. Server receives webhook payload
  2. Webhook handler checks if Event is DeclinedPayment
    • If not DeclinedPayment: Stop processing
    • If DeclinedPayment: Continue to next step
  3. Webhook handler queries transaction using transId from webhook
  4. Transaction API returns transaction details
  5. 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
  6. Webhook handler requests subscription details from Subscription API
  7. Subscription API returns subscription details
  8. Webhook handler updates subscription or retries payment
  9. Subscription API confirms operation completed

This flow enables custom retry logic for declined subscription payments. Payabli doesn’t automatically retry failed subscription payments.

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 tokenUrl = "https://api-sandbox.payabli.com/api/v2/Token/serverside";
2const url = "https://api-sandbox.payabli.com/api/Notification";
3const CLIENT_ID = "YOUR_CLIENT_ID"; // Replace with your OAuth2 client ID
4const CLIENT_SECRET = "YOUR_CLIENT_SECRET"; // Replace with your OAuth2 client secret
5
6// Function to exchange client credentials for an access token
7const getAccessToken = async () => {
8 const response = await fetch(tokenUrl, {
9 method: "POST",
10 headers: { "Content-Type": "application/json" },
11 body: JSON.stringify({ clientId: CLIENT_ID, clientSecret: CLIENT_SECRET })
12 });
13 if (!response.ok) {
14 throw new Error(`Failed to get access token: HTTP ${response.status}`);
15 }
16 const data = await response.json();
17 return data.access_token;
18};
19
20// Base payload structure
21const basePayload = {
22 content: {
23 timeZone: "-5",
24 webHeaderParameters: [
25 // Replace with your own authentication parameters
26 { key: "myAuthorizationID", value: "1234" }
27 ],
28 eventType: "declinedpayment",
29 },
30 method: "web",
31 frequency: "untilcancelled",
32 target: "https://my-app-url.com/", // Replace with your own URL
33 status: 1,
34 ownerType: 2,
35 ownerId: "255" // Replace with your own paypoint ID
36};
37
38// Function to send webhooks
39const sendWebhook = async () => {
40 const payload = basePayload;
41
42 try {
43 const accessToken = await getAccessToken();
44 const headers = {
45 "Authorization": `Bearer ${accessToken}`,
46 "Content-Type": "application/json"
47 };
48
49 const response = await fetch(url, {
50 method: "POST",
51 headers,
52 body: JSON.stringify(payload)
53 });
54
55 const responseText = await response.text();
56 console.log(`Notification for DeclinedPayment, Status: ${response.status}, Response: ${responseText}`);
57 } catch (error) {
58 console.error(`Failed to create notification for DeclinedPayment:`, error);
59 }
60};
61
62sendWebhook();

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 CLIENT_ID = "your-client-id"; // Replace with actual OAuth2 client ID
9const CLIENT_SECRET = "your-client-secret"; // Replace with actual OAuth2 client secret
10
11// API base URLs based on environment
12const API_BASE_URLS = {
13 sandbox: "https://api-sandbox.payabli.com",
14 production: "https://api.payabli.com",
15};
16
17// Define the expected webhook payload structure
18interface WebhookPayload {
19 Event?: string;
20 transId?: string;
21 [key: string]: any; // Allow additional properties
22}
23
24// Function to exchange client credentials for an access token
25const getAccessToken = async (): Promise<string> => {
26 const response = await fetch(`${API_BASE_URLS[ENVIRONMENT]}/api/v2/Token/serverside`, {
27 method: "POST",
28 headers: { "Content-Type": "application/json" },
29 body: JSON.stringify({ clientId: CLIENT_ID, clientSecret: CLIENT_SECRET }),
30 });
31 if (!response.ok) {
32 throw new Error(`Failed to get access token: HTTP ${response.status}`);
33 }
34 const data = await response.json();
35 return data.access_token;
36};
37
38// Function to handle declined payments
39const handleDeclinedPayment = async (transId?: string): Promise<void> => {
40 if (!transId) {
41 console.log("DeclinedPayment received, but it didn't include a transaction ID.");
42 return;
43 }
44
45 try {
46 const accessToken = await getAccessToken();
47 const headers = { Authorization: `Bearer ${accessToken}` };
48
49 // Fetch transaction from transId in DeclinedPayment event
50 const transactionQueryUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Query/transactions/${ENTRY}?transId(eq)=${transId}`;
51 const response = await fetch(transactionQueryUrl, { method: "GET", headers });
52 if (!response.ok) {
53 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
54 }
55
56 // Get subscription ID from transaction
57 const data = await response.json();
58 const subscriptionId = data?.Records[0]?.ScheduleReference;
59 if (!subscriptionId) {
60 console.log("DeclinedPayment notification received, but no subscription ID found.");
61 return;
62 }
63
64 await subscriptionRetry(subscriptionId, accessToken); // Perform logic on subscription with subscription ID
65 } catch (error) {
66 console.error(`Error handling declined payment: ${error}`);
67 }
68};
69
70const subscriptionRetry = async (subId: string, accessToken: string): Promise<void> => {
71 const subscriptionUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Subscription/${subId}`;
72 const headers = {
73 "Content-Type": "application/json",
74 Authorization: `Bearer ${accessToken}`,
75 };
76
77 const body = JSON.stringify({
78 setPause: false, // unpause subscription after decline
79 paymentDetails: {
80 storedMethodId: "4000e8c6-...-1323", // Replace with actual stored method ID
81 storedMethodUsageType: "recurring",
82 },
83 scheduleDetails: {
84 startDate: "2025-05-20", // Must be a future date
85 },
86 });
87
88 try {
89 const response = await fetch(subscriptionUrl, { method: "PUT", headers, body });
90 if (!response.ok) {
91 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
92 }
93 const data = await response.json();
94 console.log("Subscription updated successfully:", data);
95 } catch (error) {
96 console.error("Error updating subscription:", error);
97 }
98};
99
100const app = express();
101const PORT = 3333;
102
103// Middleware to parse JSON payloads
104app.use(express.json());
105
106// Webhook endpoint
107app.post("/webhook", (req: Request, res: Response): void => {
108 const payload: WebhookPayload = req.body;
109
110 if (payload.Event === "DeclinedPayment") {
111 handleDeclinedPayment(payload.transId);
112 }
113
114 res.sendStatus(200); // Acknowledge receipt
115});
116
117// Start server
118app.listen(PORT, () => {
119 console.log(`Server is running on port ${PORT}, Environment: ${ENVIRONMENT}`);
120});

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 CLIENT_ID = "your-client-id"; // Replace with actual OAuth2 client ID
9const CLIENT_SECRET = "your-client-secret"; // Replace with actual OAuth2 client secret
10
11// API base URLs based on environment
12const API_BASE_URLS = {
13 sandbox: "https://api-sandbox.payabli.com",
14 production: "https://api.payabli.com",
15};
16
17// Define the expected webhook payload structure
18interface WebhookPayload {
19 Event?: string;
20 transId?: string;
21 [key: string]: any; // Allow additional properties
22}
23
24// Function to exchange client credentials for an access token
25const getAccessToken = async (): Promise<string> => {
26 const response = await fetch(`${API_BASE_URLS[ENVIRONMENT]}/api/v2/Token/serverside`, {
27 method: "POST",
28 headers: { "Content-Type": "application/json" },
29 body: JSON.stringify({ clientId: CLIENT_ID, clientSecret: CLIENT_SECRET }),
30 });
31 if (!response.ok) {
32 throw new Error(`Failed to get access token: HTTP ${response.status}`);
33 }
34 const data = await response.json();
35 return data.access_token;
36};
37
38// Function to handle declined payments
39const handleDeclinedPayment = async (transId?: string): Promise<void> => {
40 if (!transId) {
41 console.log("DeclinedPayment notification received, but it didn't include a transaction ID.");
42 return;
43 }
44
45 try {
46 const accessToken = await getAccessToken();
47 const headers = { Authorization: `Bearer ${accessToken}` };
48
49 // Fetch transaction from transId in DeclinedPayment event
50 const transactionQueryUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Query/transactions/${ENTRY}?transId(eq)=${transId}`;
51 const response = await fetch(transactionQueryUrl, { method: "GET", headers });
52 if (!response.ok) {
53 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
54 }
55
56 // Get subscription ID from transaction
57 const data = await response.json();
58 const subscriptionId = data?.Records[0]?.ScheduleReference;
59 if (!subscriptionId) {
60 console.log("DeclinedPayment notification received, but no subscription ID found.");
61 return;
62 }
63
64 await subscriptionRetry(subscriptionId, accessToken); // Perform logic on subscription with subscription ID
65 } catch (error) {
66 console.error(`Error handling declined payment: ${error}`);
67 }
68};
69
70const subscriptionRetry = async (subId: string, accessToken: string): Promise<void> => {
71 const subscriptionUrl = `${API_BASE_URLS[ENVIRONMENT]}/api/Subscription/${subId}`;
72 const headers = {
73 "Content-Type": "application/json",
74 Authorization: `Bearer ${accessToken}`,
75 };
76
77 const body = JSON.stringify({
78 setPause: false, // unpause subscription after decline
79 paymentDetails: {
80 storedMethodId: "4000e8c6-...-1323", // Replace with actual stored method ID
81 storedMethodUsageType: "recurring",
82 },
83 scheduleDetails: {
84 startDate: "2025-05-20", // Must be a future date
85 },
86 });
87
88 try {
89 const response = await fetch(subscriptionUrl, { method: "PUT", headers, body });
90 if (!response.ok) {
91 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
92 }
93 const data = await response.json();
94 console.log("Subscription updated successfully:", data);
95 } catch (error) {
96 console.error("Error updating subscription:", error);
97 }
98};
99
100export default (req: NextApiRequest, res: NextApiResponse): void => {
101 if (req.method === "POST") {
102 const payload: WebhookPayload = req.body;
103
104 if (payload.Event === "DeclinedPayment") {
105 handleDeclinedPayment(payload.transId);
106 }
107
108 res.status(200).end(); // Acknowledge receipt
109 } else {
110 res.setHeader("Allow", ["POST"]);
111 res.status(405).end(`Method ${req.method} Not Allowed`);
112 }
113};

See these related resources to help you get the most out of Payabli.