Subscriptions
Set up recurring billing, manage subscription plans, and handle payment lifecycle with the PayConnect Subscriptions API.
Overview
PayConnect subscriptions automate recurring billing with built-in support for:
- Flexible billing cycles — daily, weekly, biweekly, monthly, quarterly, semiannual, or yearly
- Subscription plans — pre-configured pricing templates
- Add-ons and discounts — customize pricing per subscriber
- Automated dunning — retry failed payments with configurable schedules
- Manual payments — record offline payments (checks, cash, money orders)
- Customer self-service — hosted management page for payment method updates
Create a Subscription Plan
Before creating subscriptions, define a plan. A plan has a single billingCycle and a single price (in dollars); subscriptions inherit the plan's cycle.
- cURL
- Node.js
- Python
curl -X POST https://dev.payconnect.us/api/subscriptions/v2/plans \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: YOUR_CORRELATION_ID" \
-H "Content-Type: application/json" \
-d '{
"name": "Professional Plan",
"billingCycle": "monthly",
"price": 99.00,
"description": "Full access to all professional features"
}'
const axios = require("axios");
const response = await axios.post(
"https://dev.payconnect.us/api/subscriptions/v2/plans",
{
name: "Professional Plan",
billingCycle: "monthly",
price: 99.00,
description: "Full access to all professional features",
},
{
headers: {
"x-session-token": "YOUR_SESSION_TOKEN",
"x-correlation-id": "YOUR_CORRELATION_ID",
"Content-Type": "application/json",
},
}
);
console.log("Plan ID:", response.data.planId);
import requests
response = requests.post(
"https://dev.payconnect.us/api/subscriptions/v2/plans",
headers={
"x-session-token": "YOUR_SESSION_TOKEN",
"x-correlation-id": "YOUR_CORRELATION_ID",
"Content-Type": "application/json",
},
json={
"name": "Professional Plan",
"billingCycle": "monthly",
"price": 99.00,
"description": "Full access to all professional features",
},
)
plan_id = response.json()["planId"]
Plan Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Plan display name |
billingCycle | string | Yes | The single cycle this plan bills on: daily, weekly, biweekly, monthly, quarterly, semiannual, or yearly. Subscriptions inherit it. |
price | number | Yes | Base price in dollars charged each billing cycle (e.g., 99.00) |
description | string | No | Plan description |
planId | string | No | Custom plan ID (auto-generated if omitted) |
setupFee | number | No | One-time setup fee in dollars charged on the first invoice only |
trialDays | number | No | Number of trial days |
features | array | No | List of plan features |
active | boolean | No | Whether the plan is active (default: true) |
preRenewalInvoice | object | No | Renewal-invoice timing: { "leadTimeDays": <0 to cycle length> }. See Renewal invoice timing. |
Renewal invoice timing
Every billing cycle, PayConnect automatically generates the renewal invoice and charges it on the renewal date. preRenewalInvoice is a container object whose only field is leadTimeDays — it isn't a second, separate setting. leadTimeDays controls how many days before the renewal date that invoice is issued, so the customer gets advance notice of the upcoming charge.
- Default: 7 days.
- Range: 0 up to the billing-cycle length — the per-cycle maximums are 1 (daily), 7 (weekly), 14 (biweekly), 30 (monthly), 90 (quarterly), 182 (semiannual), and 365 (yearly). The value is capped to that length — e.g. a weekly plan issues at most 7 days early.
0means issue the invoice on the renewal date itself (no advance notice).
Automatic renewal billing always happens — this setting only changes when the invoice is issued, never whether the customer is billed. There is no way to switch renewal billing off for a plan.
{ "name": "Pro", "billingCycle": "monthly", "price": 20.00, "preRenewalInvoice": { "leadTimeDays": 3 } }
List Plans
curl -X GET "https://dev.payconnect.us/api/subscriptions/v2/plans?active=true" \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: YOUR_CORRELATION_ID"
Create a Subscription
The customer must already exist — create one first via the Customers API and pass its customerId.
- cURL
- Node.js
- Python
curl -X POST https://dev.payconnect.us/api/subscriptions/v2 \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: YOUR_CORRELATION_ID" \
-H "Content-Type: application/json" \
-d '{
"customerId": "cust_abc123",
"planId": "plan_xyz789",
"startDate": "2026-04-01"
}'
const axios = require("axios");
const response = await axios.post(
"https://dev.payconnect.us/api/subscriptions/v2",
{
customerId: "cust_abc123",
planId: "plan_xyz789",
startDate: "2026-04-01",
},
{
headers: {
"x-session-token": "YOUR_SESSION_TOKEN",
"x-correlation-id": "YOUR_CORRELATION_ID",
"Content-Type": "application/json",
},
}
);
console.log("Subscription ID:", response.data.subscriptionId);
import requests
response = requests.post(
"https://dev.payconnect.us/api/subscriptions/v2",
headers={
"x-session-token": "YOUR_SESSION_TOKEN",
"x-correlation-id": "YOUR_CORRELATION_ID",
"Content-Type": "application/json",
},
json={
"customerId": "cust_abc123",
"planId": "plan_xyz789",
"startDate": "2026-04-01",
},
)
subscription_id = response.json()["subscriptionId"]
Response (201 Created):
{
"message": "Subscription created successfully",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"subscriptionId": "sub_abc123"
}
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | PayConnect customer ID the subscription bills. The customer must already exist — create one via the Customers API first. |
planId | string | Yes | Subscription plan ID |
startDate | string | Yes | Billing start date (ISO 8601) |
billingCycle | string | No | Ignored — the subscription inherits the plan's single billing cycle. Accepted for backward compatibility; any value sent is discarded. |
billingMode | string | No | recurring (auto-charge each cycle), recurringManual (renews and invoices each cycle but the customer pays manually — never auto-charged), or nonRecurring (one-time). Defaults to recurring. See Billing modes. |
trialDays | number | No | Number of trial days before first charge. Whole number, 0–365. |
amount | number | No | Override the plan's recurring price for this subscription. When supplied, this is the recurring total — add-ons, discounts, and tax do not apply on top. Mutually exclusive with addOns/discounts. Must be greater than 0 and at most 99999.99. |
addOns | array | No | Additional products billed each cycle on top of the plan price. Cannot be combined with amount. See Add-ons. |
discounts | array | No | Applied discounts. Cannot be combined with amount. |
paymentDueDays | number | No | Grace period, in days, before a generated invoice is due (default 30). See Payment due date. |
suspendAfterDays | number | No | recurringManual only. Days after the invoice due date before an unpaid subscription moves from past_due to suspended (default 30). Ignored for other billing modes. See Manual renewal. |
customFields | object | No | Client-defined custom field values, keyed by field key |
Payment rails on subscription invoices
A subscription's paymentMethods governs the rails its hosted payment-setup
page offers. It does not decide which rails the invoices it generates accept —
that is resolved per invoice, and the customer's
defaultPaymentOptions takes precedence
over the subscription's value:
- The billed customer's
defaultPaymentOptions, if set. - Otherwise the subscription's
paymentMethods. - Otherwise both rails.
This applies to the initial invoice and to every renewal invoice.
Because the customer's setting wins for invoices while the subscription's own value
still drives its setup page, a subscription whose paymentMethods lists both rails,
for a customer set to ACH only, shows card and bank options on the setup page but
produces invoices that accept ACH only. That is expected.
subscription.paymentMethods is read-only over the API, so there is currently no
way to narrow the setup page to match. No subscription create or update request
accepts the field — sending it is silently ignored, like any unknown key — and it
defaults to both rails when a record carries no value. It is reported on subscription
reads, and the invoice resolver above consults it, so a record that does carry a value
still influences both its setup page and its invoices; you just cannot set one from
here.
A customer edit never writes subscription.paymentMethods — the subscription
record is left exactly as it was.
Payment due date (paymentDueDays)
paymentDueDays sets the grace period on the invoices a subscription generates: each invoice's due date is placed this many days after the invoice is issued. If you don't send the field, it defaults to 30 days.
It applies whether or not the subscription auto-charges:
- Auto-charging subscriptions still attempt the charge on the renewal date. If that charge fails, the invoice stays payable until its due date — giving the customer a window to pay it manually (for example, after updating their card) before it's considered overdue.
- Manual subscriptions (no saved payment method on file) issue the invoice with this due date each cycle for the customer to pay.
Renewal invoices are prepared a few days before the renewal date, so a very small paymentDueDays never makes an invoice due before its renewal/charge date — the due date is floored at the renewal date in that case.
Automatic payment retries (dunning) are independent of paymentDueDays — they run on the schedule configured for your account and are not shortened or extended by this value.
The subscription's own status reflects whether the invoice gets paid, not this field directly: an unpaid renewal parks the subscription as pending (manual, no payment method) or failed (a failed auto-charge); for recurringManual subscriptions it moves to past_due and then suspended (see Manual renewal). Paying the invoice returns any of these to active.
Example: "paymentDueDays": 45 issues invoices that come due 45 days after they are generated.
Custom amount (override the plan price)
To bill a specific amount instead of the plan's configured price, include amount. The value becomes the recurring total for that subscription — useful when each customer is billed a different figure against a shared plan. The plan still governs the trial, setup fee, and dunning policy; only the recurring price changes. amount cannot be combined with addOns or discounts.
curl -X POST https://dev.payconnect.us/api/subscriptions/v2 \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: YOUR_CORRELATION_ID" \
-H "Content-Type: application/json" \
-d '{
"customerId": "cust_abc123",
"planId": "plan_xyz789",
"startDate": "2026-04-01",
"amount": 149.99
}'
Add-ons
Each add-on line is billed every cycle on top of the plan's recurring price. An add-on always references a product — its unit price and display name resolve live from the product at billing time and cascade on product changes. You cannot set or override the price on an add-on line.
| Field | Type | Required | Description |
|---|---|---|---|
productId | string | Yes | The product this add-on references. Its price and display name resolve live at billing time, so a later change to the product automatically flows onto the subscription at its next renewal. |
quantity | integer | Yes | Number of units (must be a positive integer). |
curl -X POST https://dev.payconnect.us/api/subscriptions/v2 \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: YOUR_CORRELATION_ID" \
-H "Content-Type: application/json" \
-d '{
"customerId": "cust_abc123",
"planId": "plan_xyz789",
"startDate": "2026-04-01",
"addOns": [
{ "productId": "prod_abc123", "quantity": 2 },
{ "productId": "prod_def456", "quantity": 1 }
]
}'
Billing Modes
| Mode | Description |
|---|---|
recurring | PayConnect automatically charges the customer's stored payment method on each billing cycle |
recurringManual | Renews and generates an invoice each cycle like recurring, but PayConnect never auto-charges — the invoice is emailed and the customer pays it manually. No stored payment method is required. See Manual renewal. |
nonRecurring | PayConnect generates a single invoice the customer pays (card/ACH, or recorded as paid). The subscription activates once the invoice is paid, then terminates — no further invoices or charges. |
recurringManual is only available on plan-driven subscriptions — the custom subscription endpoint (fixed-term one-shots) accepts only recurring/nonRecurring.
Manual renewal (recurringManual)
A recurringManual subscription bills on a recurring cadence but is paid by the customer, not auto-charged. Its status is driven by whether the current invoice is paid on time:
| Status | Meaning |
|---|---|
active | The current invoice is paid; the subscription is in good standing. |
past_due | The invoice has passed its due date (paymentDueDays) and is unpaid — late but still live. |
suspended | The invoice is additionally past suspendAfterDays (counted from the due date) and still unpaid. A suspended subscription stops renewing until the invoice is paid. |
Paying the outstanding invoice (card/ACH on the hosted page, or recorded as paid) returns a past_due or suspended subscription to active and advances the cycle. suspendAfterDays defaults to 30; set it (including 0, to suspend as soon as the invoice is overdue) at create time.
Get a Subscription
curl -X GET https://dev.payconnect.us/api/subscriptions/v2/sub_abc123 \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: YOUR_CORRELATION_ID"
Response:
{
"subscriptionId": "sub_abc123",
"status": "active",
"planName": "Professional Plan",
"totalAmount": 99.00,
"billingCycle": "monthly",
"nextBillingDate": "2026-06-01T12:00:00.000Z",
"customerId": "cust_abc123"
}
Search Subscriptions
Filter and paginate subscriptions:
Results are paginated with an opaque cursor — limit bounds each page, and
you follow nextCursor until it is null. Results come back in subscription
creation order. See Paginating large result sets
below for the full contract.
curl -X POST https://dev.payconnect.us/api/subscriptions/v2/search \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: YOUR_CORRELATION_ID" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"status": { "value": "active", "operator": "eq" }
},
"limit": 20
}'
Response (201 Created):
{
"subscriptions": [
{
"subscriptionId": "sub_abc123",
"status": "active",
"planName": "Professional Plan",
"totalAmount": 99.00,
"billingCycle": "monthly",
"nextBillingDate": "2026-06-01T12:00:00.000Z"
}
],
"nextCursor": null,
"hasMore": false,
"truncated": false
}
nextCursor is the pagination cursor — a string until the last page, then
null (the only completion signal). hasMore mirrors nextCursor != null and
is the loop condition. truncated is true only when a customer free-text
filter's directory lookup was itself too large to read in full — the
subscription set is always paged to completion, so paging can never be the source
of truncation. A Warning response header is also set when truncated is true.
Available Filters
| Filter | Operators | Description |
|---|---|---|
status | eq, in | Subscription status |
billingCycle | eq, in | Billing frequency |
billingMode | eq | recurring, recurringManual, or nonRecurring |
totalAmount | eq, gt, gte, lt, lte, between | Amount in dollars |
nextBillingDate | eq, gt, gte, lt, lte, between | Next charge date |
createdAt | eq, gt, gte, lt, lte, between | Creation date |
updatedAt | eq, gt, gte, lt, lte, between | Last-modified date — see below |
email | eq, contains, startsWith | Customer email |
customerId | eq, in | Customer ID |
planId | eq | Plan ID |
Filtering by updatedAt
Use updatedAt to pull only the subscriptions that changed since you last
looked, instead of re-reading the full list.
curl -X POST https://dev.payconnect.us/api/subscriptions/v2/search \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"updatedAt": { "value": [1757462400000, 1757548800000], "operator": "between" }
},
"limit": 100
}'
Timestamps compare exactly; date-only strings cover a whole day. A number
(epoch milliseconds) or a full datetime string such as
2026-09-10T14:30:00.000Z is matched to the millisecond. A date-only string
such as 2026-09-10 covers that entire UTC day — with gte/lt anchored to
the start of the day and gt/lte to its end, so the day you name is always
inside the range you asked for. A datetime with no offset is read as UTC, so
include one (Z or +02:00) if you mean another zone. A value that cannot be
parsed as a date returns a 400 rather than quietly returning everything.
Give between its bounds in order. [start, end] with start no later
than end; a reversed range returns a 400 rather than an empty page, so a
swapped range shows up as an error instead of looking like "nothing changed".
What counts as an update. Any write bumps updatedAt, including mechanical
ones — an email event, a status sweep, a renewal charge. Delivery is
at-least-once: a retried payment postback legitimately re-bumps the timestamp
and the subscription appears again, so dedupe by subscriptionId.
This is a bounded read, not a change feed. Poll a closed interval —
between with both bounds in the past — rather than an open gte against
"now": a subscription modified while the response is being assembled would
otherwise be missed by the next poll. Read every match by paging the cursor
until nextCursor is null (see "Paginating large result sets" below) — that
is the only completion signal for the subscription result set; a single
limit-sized response is not the full set. Note truncated does not cover
this — it reflects only a bounded customer-directory lookup behind a customer
filter, not the subscription page, so never treat truncated: false as "I have
every subscription." Follow nextCursor to null instead.
Paginating large result sets (limit / cursor)
Search is cursor-paginated. Each request returns up to limit matches
(1–100, default 20) plus an opaque nextCursor; you read the complete result set
by following nextCursor until it comes back null. There is no page number and
no total count — a keyset cursor reads only what it returns, which is what lets it
stay fast and complete on an account of any size, without the multi-thousand-row
cap that a materialize-and-slice page-number scan hits. (The older page /
pageSize inputs were removed; sending either returns a 400. sortBy /
sortOrder are accepted only on a customer-scoped search. See
Sorting a customer's subscriptions.)
Under a sustained burst of search traffic the endpoint may return 429 with a
Retry-After header — back off for that many seconds and retry the same
request (same cursor); it is a transient capacity signal, not a bad request.
# First page: omit `cursor` (an empty string "" also works).
curl -X POST https://dev.payconnect.us/api/subscriptions/v2/search \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filters": { "updatedAt": { "value": [1757462400000, 1757548800000], "operator": "between" } },
"limit": 100
}'
Response — the flat cursor envelope:
{
"subscriptions": [ /* up to `limit` matches, in creation order */ ],
"nextCursor": "<opaque-cursor-from-this-response>",
"hasMore": true,
"truncated": false
}
The nextCursor value is an opaque, versioned token — do not parse, construct,
or persist it; treat it as a string and pass it back verbatim as cursor to
fetch the next page. Repeat while hasMore is true (equivalently, until
nextCursor is null).
Things to know about cursor pagination:
nextCursor: nullis the ONLY completion signal. A page may contain fewer thanlimititems — even zero — whilenextCursoris still non-null (a sparse filter can scan a full internal budget without filling a page). Never infer "done" from an empty or short page; loop onhasMore/ page untilnextCursorisnull. A client that stops on an empty array silently misses rows.- No total count. A keyset cursor cannot cheaply report a grand total, so
there is no
totalItems/totalPages— completeness is driven bynextCursor, never by a page count. - Results come back in creation order. A company-wide search's order is fixed (append order). For a change-driven sync this is ideal — you page the whole set and order it yourself. A search scoped to customers can instead be sorted server-side (below).
- Changing
limitbetween pages is safe. The cursor encodes position, not an offset, so you can grow or shrinklimitmid-traversal without skipping or repeating rows. - The
customerfree-text filter is supported. It resolves to the matching customers and pages their subscriptions in full. If the customer directory itself is too large to read completely, the response setstruncated: true(with acustomerSearchblock giving the details) — the one case where some matching subscriptions may be missing. - Custom-field filters are not index-accelerated in cursor mode. Page-number search can serve an indexed custom-field filter from a secondary index; cursor mode deliberately does not, because that index is itself capped and so cannot back cursor mode's complete-results guarantee. A custom-field-filtered cursor read therefore scans the subscription partition in pages rather than doing a targeted index lookup — a deliberate trade of index speed for completeness. For a large tenant filtering on an indexed custom field where a truncated, index-fast result is acceptable, page-number search remains the faster option.
Sorting a customer's subscriptions (sortBy / sortOrder)
A search scoped to customers can be sorted server-side. It counts as scoped when
it has a customerId filter with eq, or in with up to 100 ids, or a
customer filter that matches at most 100 customers:
{
"filters": { "customerId": { "value": "cust_abc123", "operator": "eq" } },
"sortBy": "nextBillingDate",
"sortOrder": "asc",
"limit": 25
}
sortByis one ofnextBillingDate,createdAt,status, ortotalAmount. Plan name is not sortable.sortOrderisasc(the default) ordesc. Sending it withoutsortByreturns a400.- Ties are broken by subscription id, and subscriptions missing the sort field come
last in either direction, so the order is fully deterministic.
statussorts alphabetically by its value (active,cancelled,ended, …). - Page with
nextCursorexactly as above. The cursor carries the sort, so passing it back with a differentsortByorsortOrder(or withoutsortBy) returns a400. Keep the sort fixed for a whole traversal. As with every cursor search, its last page hashasMore: falseeven when it is exactly full (no trailing empty page). - Sort on a value that can change mid-traversal at your own risk. Each page
re-reads the live set and resumes after the last row you received. If a
subscription's sort value changes between your requests (a renewal moves its
nextBillingDate, dunning changes itsstatus, an update changes itstotalAmount), it's placed by its new value: it can show up twice or be skipped. OnlycreatedAtgives a complete read, because it never changes. On the other sorts, deduping bysubscriptionIdremoves a repeat but cannot recover a skipped row; page quickly to shrink the window, and re-read withcreatedAtwhen you need every row. - Sorting requires the customer scope.
sortByon a company-wide search (no customer filter, or more than 100 customers) returns a400. Company-wide results always come back in creation order, because sorting them would mean reading the entire account on every page. - A sorted search reads the scoped customers' whole subscription set on every page,
so a scope holding more than 10,000 subscriptions (counted before your other
filters, since they don't reduce the read) returns a
400asking you to narrow the customer set, or to omitsortBy. - While sorting is rolling out, an environment where it is not yet enabled returns
a
400saying sorting is not available yet, before reading anything. OmitsortByand sort the results yourself; the same search withoutsortByalways works.
Worked example: delta-sync
To keep a local mirror current, combine the updatedAt filter (what changed)
with cursor pagination (read all of it):
- Pick a closed interval
[lastSyncedAt, now](both in the past). - Search with that
updatedAtbetweenfilter andcursor: "". - Upsert each returned subscription into your store, keyed by
subscriptionId. - If
nextCursoris non-null, search again with the same filter andcursor: nextCursor; repeat until it isnull. - Advance
lastSyncedAttonowfor the next run.
Because delivery is at-least-once (a retried postback re-bumps updatedAt), the
same subscription can appear more than once across runs — dedupe by
subscriptionId on upsert. Paging to nextCursor: null guarantees you saw
every subscription that changed in the interval, with no silent truncation.
Summarize a Customer's Subscriptions
POST /api/subscriptions/v2/summary returns subscription counts and recurring
value for one customer or a small group of customers, in one request. Use it for
dashboards and badges ("3 subscriptions · 2 active") instead of paging a whole
list just to count it. Search responses deliberately carry no totals; this is where
they come from.
The request takes the same typed filters as Search Subscriptions,
and must be scoped to customers: customerId with eq (one id) or in (up to
100 ids), or a customer filter matching at most 100 customers. Any other
filters narrow the summary exactly as they narrow a search:
{
"filters": {
"customerId": { "value": ["cust_abc123", "cust_def456"], "operator": "in" },
"billingCycle": { "value": "monthly", "operator": "eq" }
}
}
Response (HTTP 201):
{
"byStatus": [
{ "status": "active", "count": 2, "totalAmount": 59.98 },
{ "status": "cancelled", "count": 1, "totalAmount": 19.99 }
],
"total": { "count": 3, "totalAmount": 79.97 }
}
| Field | Description |
|---|---|
byStatus | One bucket per subscription status that has at least one matching subscription, in a fixed status order. Empty when nothing matches. |
byStatus[].count | Number of matching subscriptions in that status. |
byStatus[].totalAmount | Sum of the subscriptions' totalAmount (the per-cycle charge). |
total.count, total.totalAmount | Totals across every status. |
Amounts are in the same units as a subscription's totalAmount, rounded to cents.
Subscriptions on different billing cycles are summed as stored, not converted to a
common period, so add a billingCycle filter when you need a monthly or annual
figure. To count only live subscriptions, filter by status or read the matching
bucket.
- A request without a customer scope, or with more than 100 customers, returns a
400. The endpoint never aggregates a whole account. - A
customerfilter whose customer lookup is too large to read completely returns a400(the counts would otherwise be silently low): filter bycustomerIdinstead. - The summary reads every subscription of the customers in scope, so a scope holding
more than 10,000 subscriptions (counted before your other filters) returns a
400asking you to narrow the customer set. - While this feature is rolling out, an environment where it is not yet enabled
returns a
400saying summaries are not available yet, before reading anything. Count by paging Search Subscriptions instead. - Under a sustained burst of traffic it may return
429with aRetry-Afterheader: back off for that many seconds and retry the same request.
Manage a Subscription
Use the PUT endpoint with an operation field to manage subscription state.
Cancel
curl -X PUT https://dev.payconnect.us/api/subscriptions/v2/sub_abc123 \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: YOUR_CORRELATION_ID" \
-H "Content-Type: application/json" \
-d '{
"operation": "cancel",
"cancelAtPeriodEnd": true,
"reason": "Customer requested cancellation"
}'
Set cancelAtPeriodEnd: true to let the subscription run until the current billing period ends. Set it to false for immediate cancellation.
Update
curl -X PUT https://dev.payconnect.us/api/subscriptions/v2/sub_abc123 \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: YOUR_CORRELATION_ID" \
-H "Content-Type: application/json" \
-d '{
"operation": "update",
"updates": {
"amount": 149.00,
"billingCycle": "yearly"
}
}'
Record a Manual Payment
Manual/offline payments (check, cash, money order) are recorded against an invoice from the PayConnect dashboard — there is no public API endpoint for recording a manual payment on a subscription. Open the invoice in the dashboard and use Record Manual Payment there; the recorded payment is reflected on the subscription's linked invoices.
Pay a Subscription
Charge a stored payment method for a subscription:
curl -X POST https://dev.payconnect.us/api/subscriptions/v2/sub_abc123/pay \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: YOUR_CORRELATION_ID" \
-H "Content-Type: application/json" \
-d '{
"accountVaultId": "av_xyz789",
"amount": 99.00,
"savePaymentMethod": true
}'
Response:
{
"message": "Payment processed successfully",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"subscriptionId": "sub_abc123",
"transactionId": "trx_def456",
"status": "success"
}
Subscription Statuses
| Status | Description |
|---|---|
pending | Subscription created, awaiting first payment or start date |
active | Subscription is active and billing |
cancelled | Subscription cancelled |
failed | Payment failed (enters dunning) |
ended | Subscription completed its term |
Dunning (Failed Payment Retry)
When a recurring payment fails, PayConnect automatically retries according to your dunning schedule:
- Initial failure — subscription status changes to
failed - Retry attempts — PayConnect retries the charge on a configured schedule
- Email notifications — customers receive payment failure and retry notifications
- Final failure — after all retries are exhausted, the subscription may be suspended
Dunning schedules are configured per company. Contact [email protected] to customize retry intervals and maximum attempts.
Customer Self-Service
PayConnect provides a hosted subscription management page where your customers can:
- View subscription details and billing history
- Update their payment method
- Add or remove stored payment methods
The management page uses a secure authentication flow:
- Customer visits the management URL with their subscription token
- PayConnect sends a verification code via email or SMS
- Customer enters the code to authenticate
- Customer can view and manage their subscription
Next Steps
- Customers — manage customer records and payment methods
- Invoices — send one-time payment requests
- Postbacks — receive subscription event notifications
- API Reference — full endpoint documentation