Webhooks
Listen to events on your SubscriptionFlow account so your integration can automatically trigger reactions.
SubscriptionFlow uses webhooks to notify your application in real time when something happens -- a subscription is renewed, an invoice is paid, a payment method expires, and more. Instead of polling the API, you receive an HTTP POST request at a URL you choose.
How webhooks work
- An event occurs in your SubscriptionFlow account (e.g., a customer's subscription renews).
- SubscriptionFlow creates a webhook payload containing the event type and the full or custom entity data.
- SubscriptionFlow sends an HTTP request to each webhook endpoint registered for that event.
- Your server receives the request, processes it, and returns a
2xxstatus code to acknowledge receipt.
Quickstart
Follow these steps to start receiving webhooks in under 5 minutes.
Step 1: Create your endpoint
Build an HTTP endpoint on your server that can receive POST requests. SubscriptionFlow sends webhook data as application/x-www-form-urlencoded form parameters.
const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: true }));
app.post("/webhooks/subscriptionflow", (req, res) => {
const event = req.body.event;
const method = req.body.method;
// Route to the appropriate handler based on event type
switch (event) {
case "renewed":
handleSubscriptionRenewal(req.body);
break;
case "paid":
handleInvoicePaid(req.body);
break;
case "failed":
handleTransactionFailed(req.body);
break;
default:
console.log(`Unhandled event type: ${event}`);
}
// Acknowledge receipt immediately
res.status(200).send("OK");
});
app.listen(4242, () => console.log("Webhook server running on port 4242"));
<?php
// Read the incoming webhook data
$event = $_POST['event'] ?? null;
$method = $_POST['method'] ?? null;
// Route to the appropriate handler
switch ($event) {
case 'renewed':
handleSubscriptionRenewal($_POST);
break;
case 'paid':
handleInvoicePaid($_POST);
break;
case 'failed':
handleTransactionFailed($_POST);
break;
default:
error_log("Unhandled event type: " . $event);
}
// Acknowledge receipt
http_response_code(200);
echo 'OK';
from flask import Flask, request
app = Flask(__name__)
@app.route("/webhooks/subscriptionflow", methods=["POST"])
def webhook():
event = request.form.get("event")
if event == "renewed":
handle_subscription_renewal(request.form)
elif event == "paid":
handle_invoice_paid(request.form)
elif event == "failed":
handle_transaction_failed(request.form)
else:
print(f"Unhandled event type: {event}")
return "OK", 200
Tip: Your endpoint should return a
2xxresponse as quickly as possible. Move any heavy processing to a background job or queue to avoid timeouts.
Step 2: Register the webhook in SubscriptionFlow
- Go to Settings > Webhooks in your SubscriptionFlow dashboard.
- Click Create Webhook.
- Configure the webhook:
| Field | Description |
|---|---|
| Webhook Name | A descriptive label (e.g., "Renewal notifications") |
| Webhook URL | Your HTTPS endpoint (e.g., https://example.com/webhooks/subscriptionflow) |
| Module | The entity type to listen to -- Subscription, Invoice, Transaction, Payment Method, or Email |
| Event | The specific event that triggers this webhook (e.g., renewed, paid) |
| Payload Type | DEFAULT for full entity data, or CUSTOM for selected fields only |
| Description | Optional notes about this webhook's purpose |
- Click Save.
Important: Webhook URLs must use HTTPS with a valid SSL certificate. HTTP endpoints are not accepted.
Step 3: Test your webhook
To verify your endpoint is working:
- Create a test subscription or trigger the relevant action in your SubscriptionFlow account.
- Check your webhook logs at Settings > Webhooks > Logs to confirm delivery.
- Verify your server received and processed the request.
You can also use tools like webhook.site to inspect the raw payload before pointing the webhook at your production server.
Events
Every webhook request includes an event field that tells you what happened. Events are grouped by module.
Subscription events
| Event | Trigger |
|---|---|
subscription.created |
A new subscription is created |
subscription.updated |
A subscription's details are modified |
subscription.deleted |
A subscription is deleted |
subscription.renewed |
A subscription auto-renews at the end of its billing cycle |
subscription.suspended |
A subscription is suspended (manually or automatically) |
subscription.expired |
A termed subscription passes its billing end date |
subscription.cancelled |
A subscription is cancelled |
subscription.resumed |
A suspended subscription is reactivated |
subscription.term_started |
A new billing term begins on a termed subscription |
subscription.entitlement_redeemed |
An entitlement on the subscription is redeemed |
subscription.upgraded |
A subscription moves to a higher-tier plan |
subscription.downgraded |
A subscription moves to a lower-tier plan |
Invoice events
| Event | Trigger |
|---|---|
invoice.created |
A new invoice is generated |
invoice.updated |
An invoice is modified |
invoice.deleted |
An invoice is deleted |
invoice.overdue |
An invoice passes its due date without payment |
invoice.paid |
An invoice is fully paid |
invoice.reversed_charges |
Charges on an invoice are reversed |
Transaction events
| Event | Trigger |
|---|---|
transaction.created |
A new payment transaction is recorded |
transaction.deleted |
A transaction is deleted |
transaction.failed |
A payment attempt fails |
Payment method events
| Event | Trigger |
|---|---|
paymentmethod.created |
A new payment method is added to a customer |
paymentmethod.updated |
A payment method's details are modified |
paymentmethod.deleted |
A payment method is removed |
paymentmethod.expired |
A payment method passes its expiry date |
paymentmethod.activated |
A payment method is activated |
Email events
| Event | Trigger |
|---|---|
emails.created |
An email record is created |
emails.updated |
An email record is modified |
emails.deleted |
An email record is deleted |
emails.failed |
An email fails to deliver |
emails.bounced |
An email bounces back |
Payload format
Every webhook request contains the event data. The shape of the payload depends on the payload type you select when creating the webhook.
Default payload
The default payload sends the full entity representation -- the same data you'd get from the corresponding REST API endpoint.
Example: subscription.renewed
{
"type": "subscription",
"id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"attributes": {
"name": "SUB-00142",
"display_name": "Pro Monthly - John Doe",
"status": "active",
"type": "auto_recurring",
"total_amount": "49.99",
"next_bill_date": "2026-05-01",
"billing_end_date": null,
"is_auto_renew": true,
"renewal_type": "auto",
"renewal_period": "1",
"renewal_period_type": "month",
"renewed_at": "2026-04-01",
"trial_period": null,
"trial_expiry": null,
"subscription_mrr": "49.99",
"payment_status": "paid",
"tags": ["premium", "annual-promo"],
"created_at": "2025-01-15T10:30:00.000000Z",
"updated_at": "2026-04-01T00:00:12.000000Z"
},
"relationships": {
"customer_id": {
"type": "customer",
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"attributes": {
"first_name": "John",
"last_name": "Doe",
"primary_email": "john.doe@example.com"
}
}
},
"event": "renewed",
"method": "POST"
}
Example: invoice.paid
{
"type": "invoice",
"id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"attributes": {
"name": "INV-00318",
"status": "paid",
"invoice_date": "2026-04-01",
"due_date": "2026-04-15",
"currency": "USD",
"sub_total": "49.99",
"tax_amount": "4.50",
"total_amount": "54.49",
"outstanding_amount": "0.00",
"received_payment": "54.49",
"description": "Pro Monthly subscription renewal",
"created_at": "2026-04-01T00:00:12.000000Z",
"updated_at": "2026-04-01T00:01:05.000000Z"
},
"relationships": {
"customer_id": {
"type": "customer",
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"attributes": {
"first_name": "John",
"last_name": "Doe",
"primary_email": "john.doe@example.com"
}
}
},
"event": "paid",
"method": "POST"
}
Example: transaction.failed
{
"type": "transaction",
"id": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"attributes": {
"name": "TXN-00587",
"number": "587",
"status": "failed",
"amount": "54.49",
"currency": "USD",
"date": "2026-04-01",
"type": "charge",
"decline_reason": "insufficient_funds",
"reason_code": "card_declined",
"created_at": "2026-04-01T00:01:00.000000Z",
"updated_at": "2026-04-01T00:01:00.000000Z"
},
"relationships": {
"customer_id": {
"type": "customer",
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"attributes": {
"first_name": "John",
"last_name": "Doe",
"primary_email": "john.doe@example.com"
}
}
},
"event": "failed",
"method": "POST"
}
Example: paymentmethod.expired
{
"type": "paymentmethod",
"id": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
"attributes": {
"name": "Visa ending 4242",
"status": "expired",
"payment_method_type": "credit_card",
"payment_method_subtype": "visa",
"last_four_digits": "4242",
"expiry_month": "03",
"expiry_year": "2026",
"gateway_name": "Stripe",
"default": true,
"first_name": "John",
"last_name": "Doe",
"billing_email": "john.doe@example.com",
"created_at": "2025-01-15T10:32:00.000000Z",
"updated_at": "2026-04-01T00:00:00.000000Z"
},
"relationships": {
"parent_id": {
"type": "customer",
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
}
},
"event": "expired",
"method": "POST"
}
Custom payload
Custom payloads let you control exactly which fields are sent. You map fields using the template syntax {{ModelName_fieldname}} and add any static key-value pairs you need.
Configuration example:
| Key | Value template |
|---|---|
sub_id |
{{Subscription_id}} |
email |
{{Customer_email}} |
plan |
{{Subscription_plan}} |
status |
{{Subscription_status}} |
source |
subscriptionflow (static) |
Resulting payload:
{
"sub_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"email": "john.doe@example.com",
"plan": "Pro Monthly",
"status": "active",
"source": "subscriptionflow",
"event": "renewed",
"method": "POST"
}
Custom payloads also support the GET method. When using GET, data is sent as query string parameters instead of a POST body.
Delivery details
| Property | Value |
|---|---|
| HTTP method | POST (default) or GET (custom payloads only) |
| Content type | application/x-www-form-urlencoded |
| Timeout | 30 seconds |
| Protocol | HTTPS required (valid SSL certificate) |
Retry behavior
| Response | Behavior |
|---|---|
2xx |
Delivery confirmed. No retry. |
404 |
Permanently failed. No retry. |
| Other errors | Retried automatically based on your queue configuration. |
Fields included in every request
Regardless of payload type, every webhook request includes:
| Field | Type | Description |
|---|---|---|
event |
string | The event name (e.g., renewed, paid, failed) |
method |
string | The HTTP method used (GET or POST) |
Full field reference
These are all fields available in the DEFAULT payload for each module.
Subscription fields
Attributes
| Field | Type | Description |
|---|---|---|
id |
uuid | Unique identifier |
name |
string | Subscription identifier (e.g., SUB-00142) |
display_name |
string | Human-readable display name |
status |
string | active, suspended, cancelled, expired |
type |
string | Subscription type |
payment_status |
string | Current payment status |
total_amount |
decimal | Total subscription amount |
shipping_charge |
decimal | Shipping charges |
total_shipping_charges |
decimal | Accumulated shipping charges |
subscription_mrr |
decimal | Monthly Recurring Revenue |
remaining_term_amount |
decimal | Remaining amount for current term |
remaining_term_payments |
integer | Remaining payments for current term |
current_term_payments |
integer | Payments made in current term |
next_bill_date |
date | Next billing date |
billing_end_date |
date | Billing period end date |
start_issue |
integer | Starting issue number |
end_issue |
integer | Ending issue number |
number_of_issues |
integer | Total number of issues |
remaining_issues |
integer | Remaining issues |
remaining_days |
integer | Remaining days in the term |
is_auto_renew |
boolean | Whether auto-renewal is enabled |
renewal_type |
string | Renewal type |
renewal_period |
integer | Renewal period length |
renewal_period_type |
string | day, month, year |
renewal_price_change_type |
string | How price changes on renewal |
renewal_price_change_value |
decimal | Price change amount or percentage |
renewed_at |
datetime | Last renewal timestamp |
active_period |
integer | Active period length |
trial_period |
integer | Trial period length |
trial_period_unit |
string | Trial period unit |
trial_expiry |
date | Trial expiration date |
termed_initial_period |
integer | Initial term length |
termed_initial_period_type |
string | Initial term unit |
termed_start_date |
date | Term start date |
suspended_at |
date | When the subscription was suspended |
resume_suspended_at |
date | When the suspension will lift |
cancelled_at |
date | When the subscription was cancelled |
invoice_separate |
boolean | Whether invoices are generated separately |
skip_projected_invoice |
boolean | Whether projected invoices are skipped |
is_gift |
boolean | Whether this is a gift subscription |
is_extend_period |
boolean | Whether an extension period is active |
extend_period |
integer | Extension period length |
extend_period_type |
string | Extension period unit |
extend_period_start_date |
date | Extension start date |
extend_period_expiry |
date | Extension expiry date |
extend_period_allocated_days |
integer | Total allocated extension days |
extend_period_used_days |
integer | Used extension days |
extend_period_remaining_days |
integer | Remaining extension days |
tags |
array | Associated tags |
trigger_dates |
object | Configured trigger dates |
additional_data |
object | Custom data |
data_source |
string | Data source |
default_payment_method |
uuid | Default payment method ID |
created_at |
datetime | Creation timestamp |
updated_at |
datetime | Last update timestamp |
Relationships
| Field | Related entity |
|---|---|
customer_id |
Customer |
assigned_to |
User |
assigned_group_id |
Group |
created_by |
User |
updated_by |
User |
recipient_id |
Contact (gift recipient) |
tax_id |
Tax |
invoice_status |
Invoice |
Invoice fields
Attributes
| Field | Type | Description |
|---|---|---|
id |
uuid | Unique identifier |
name |
string | Invoice number (e.g., INV-00318) |
status |
string | draft, open, paid, overdue, void |
invoice_date |
date | Issue date |
due_date |
date | Payment due date |
currency |
string | Invoice currency code |
sub_total |
decimal | Subtotal before tax and discounts |
total_amount |
decimal | Total amount due |
tax_amount |
decimal | Tax amount |
tax_breakdown |
object | Detailed tax breakdown |
discount_value |
decimal | Discount applied |
shipping_charge |
decimal | Shipping charges |
miscellaneous_charges |
decimal | Miscellaneous charges |
miscellaneous_charges_breakdown |
object | Miscellaneous charges detail |
outstanding_amount |
decimal | Remaining unpaid amount |
received_payment |
decimal | Total payments received |
opening_balance |
decimal | Opening balance |
closing_balance |
decimal | Closing balance |
sum_of_credit_notes |
decimal | Total credit notes applied |
description |
string | Invoice description |
note |
string | Notes |
terms |
string | Payment terms |
is_oneoff |
boolean | One-off invoice |
additional_info |
object | Additional information |
additional_data |
object | Custom data |
reversed_reference |
string | Reference to reversed charges |
accounting_activity_status |
string | Accounting sync status |
accounting_sync_details |
object | Accounting sync details |
ecommerce_sync_details |
object | E-commerce sync details |
shipstation_order |
string | ShipStation order reference |
shipstation_order_status |
string | ShipStation order status |
data_source |
string | Data source |
created_at |
datetime | Creation timestamp |
updated_at |
datetime | Last update timestamp |
Relationships
| Field | Related entity |
|---|---|
customer_id |
Customer |
assigned_to |
User |
assigned_group_id |
Group |
created_by |
User |
updated_by |
User |
transaction_status |
Transaction |
Transaction fields
Attributes
| Field | Type | Description |
|---|---|---|
id |
uuid | Unique identifier |
name |
string | Transaction name |
number |
string | Transaction number |
transaction_id |
string | External transaction identifier |
reference_transaction_id |
string | Reference to a related transaction |
reference |
string | Transaction reference string |
type |
string | Transaction type |
status |
string | success, failed, pending |
amount |
decimal | Transaction amount |
balance |
decimal | Remaining balance |
unapplied_amount |
decimal | Unapplied amount |
currency |
string | Currency code |
date |
date | Transaction date |
cash_or_card |
string | Payment medium |
description |
string | Description |
decline_reason |
string | Reason for decline |
reason_code |
string | Reason code |
payment_method_id |
uuid | Associated payment method |
payment_type_id |
uuid | Payment type |
transaction_category |
string | Category |
approval_link |
string | Approval link |
miscellaneous_charges |
decimal | Miscellaneous charges |
miscellaneous_charges_breakdown |
object | Miscellaneous charges detail |
accounting_account_code |
string | Accounting account code |
data_source |
string | Data source |
unpublished_id |
string | Unpublished identifier |
created_at |
datetime | Creation timestamp |
updated_at |
datetime | Last update timestamp |
Relationships
| Field | Related entity |
|---|---|
customer_id |
Customer |
assigned_to |
User |
assigned_group_id |
Group |
created_by |
User |
updated_by |
User |
Payment method fields
Attributes
| Field | Type | Description |
|---|---|---|
id |
uuid | Unique identifier |
name |
string | Payment method name |
status |
string | active, expired, inactive |
payment_method_type |
string | Type (e.g., credit_card, bank_account) |
payment_method_subtype |
string | Subtype (e.g., visa, mastercard) |
payment_method_id |
string | External identifier |
gateway |
string | Gateway identifier |
gateway_name |
string | Gateway display name |
default |
boolean | Default payment method |
last_four_digits |
string | Last four digits |
expiry_date |
string | Expiry date |
expiry_month |
string | Expiry month |
expiry_year |
string | Expiry year |
first_name |
string | Cardholder first name |
last_name |
string | Cardholder last name |
email |
string | Associated email |
phone |
string | Associated phone |
billing_name |
string | Billing name |
billing_email |
string | Billing email |
billing_address1 |
string | Address line 1 |
billing_address2 |
string | Address line 2 |
billing_address3 |
string | Address line 3 |
billing_city |
string | City |
billing_state |
string | State/province |
billing_country |
string | Country |
billing_postalcode |
string | Postal/zip code |
currency |
string | Currency |
customer_profile_id |
string | Gateway customer profile ID |
mandate |
string | Direct debit mandate reference |
routing_number |
string | Bank routing number |
source_company |
string | Source company |
owner_type |
string | Owner type |
parent_type |
string | Parent entity type |
description |
string | Description |
note |
string | Notes |
approval_link |
string | Approval link |
declined_reason |
string | Decline reason |
reason_code |
string | Reason code |
data_source |
string | Data source |
created_at |
datetime | Creation timestamp |
updated_at |
datetime | Last update timestamp |
Relationships
| Field | Related entity |
|---|---|
parent_id |
Customer |
assigned_to |
User |
created_by |
User |
updated_by |
User |
Email fields
Attributes
| Field | Type | Description |
|---|---|---|
id |
uuid | Unique identifier |
subject |
string | Email subject line |
from |
string | Sender email |
to |
string | Recipient email(s) |
cc |
string | CC recipients |
bcc |
string | BCC recipients |
reply_to |
string | Reply-to address |
email_body |
string | HTML email body |
email_text |
string | Plain text body |
excerpt |
string | Email preview text |
status |
string | sent, failed, bounced, queued |
type |
string | Email type |
tracking_status |
string | Tracking status (opened, clicked, etc.) |
failure_reason |
string | Failure reason |
message_id |
string | Email message ID |
posted_at |
datetime | Send timestamp |
is_archived |
boolean | Whether archived |
is_starred |
boolean | Whether starred |
is_guest |
boolean | Whether from a guest |
additional_payload |
object | Additional data |
data_source |
string | Data source |
created_at |
datetime | Creation timestamp |
updated_at |
datetime | Last update timestamp |
Relationships
| Field | Related entity |
|---|---|
recipientable_id |
Customer / Contact |
relatable_id |
Related entity |
assigned_to |
User |
created_by |
User |
updated_by |
User |
Webhook logs
Enable logging from Settings > Webhooks to record all delivery attempts. Each log entry includes:
| Field | Description |
|---|---|
| Status | completed or failed |
| Level | success or error |
| HTTP status code | The response code from your endpoint |
| Error message | Details when delivery fails |
| Payload | The URL and data that was sent |
Use logs to debug failed deliveries, inspect payloads, and confirm your endpoint is processing events correctly.
Best practices
Return a 2xx quickly. Your endpoint should acknowledge receipt within 30 seconds. Offload any heavy processing to a background job.
Handle duplicate deliveries. In rare cases, the same event may be sent more than once. Use the entity id and event to deduplicate on your end.
Keep your SSL certificate valid. SubscriptionFlow requires HTTPS. If your certificate expires, webhook delivery will fail.
Use DEFAULT payloads for complete data. The DEFAULT payload mirrors the API response and includes all fields and relationships. Use CUSTOM payloads only when you need to match a specific external format or limit the data sent.
Monitor your logs. Enable webhook logging and review it regularly to catch delivery failures before they impact your integration.
Don't rely on delivery order. Webhooks may arrive out of order. Use timestamps like updated_at to determine the latest state of an entity.