Webhooks
Webhooks allow you to receive real-time notifications when important subscription lifecycle events occur in your server. By configuring an HTTPS endpoint, Subscord will automatically send event data to your URL whenever key actions happen — enabling integrations with external systems like CRMs, analytics platforms, automation tools, and custom dashboards.
Supported Events
Subscord sends webhook notifications for the following events:
| Event | Description |
|---|---|
subscription.created | A new customer subscribes to a product |
subscription.renewed | An existing subscription is renewed (crypto or Stripe auto-renewal) |
subscription.expired | A subscription has expired and access has been removed |
trial.converted | A free trial has been converted to a paid subscription |
transaction.completed | A payment has been successfully processed |
Setting Up a Webhook
To configure a webhook for your server:
- Navigate to the Settings page in your Subscord Dashboard.
- Scroll to the Webhooks section.
- Click the Add Webhook button.
- Enter your Endpoint URL — this must be a valid HTTPS URL.
- Optionally, give your endpoint a Name (e.g., "My CRM Integration") for easy identification.
- Click Save.
Once saved, Subscord will generate a Signing Secret for your endpoint. You will need this secret to verify incoming webhook payloads on your server.
Important: Your webhook endpoint must use HTTPS. HTTP URLs are not accepted.
Signing Secret
Each webhook endpoint is assigned a unique signing secret upon creation. This secret is used to verify that incoming requests genuinely originate from Subscord.
- The signing secret is displayed in a masked format in your dashboard for security.
- You can copy the full secret using the copy button.
- If you believe your secret has been compromised, you can regenerate it at any time. Note that regenerating the secret will invalidate the previous one — make sure to update your integration immediately.
Verifying Webhook Signatures
Every webhook request from Subscord includes the following headers:
| Header | Description |
|---|---|
X-Subscord-Signature | HMAC-SHA256 signature of the payload |
X-Subscord-Timestamp | Unix timestamp (seconds) when the event was sent |
X-Subscord-Event | The event type (e.g., subscription.created) |
Signature Verification
The signature is computed using the following formula:
HMAC-SHA256(timestamp + "." + payload, signingSecret)Where:
timestampis the value from theX-Subscord-Timestampheader.payloadis the raw JSON request body.signingSecretis your endpoint's signing secret.
To verify a webhook request:
- Extract the
X-Subscord-TimestampandX-Subscord-Signatureheaders. - Read the raw request body as a string.
- Concatenate the timestamp, a period (
.), and the raw body. - Compute the HMAC-SHA256 hash of this string using your signing secret.
- Compare the computed hash with the value in
X-Subscord-Signature.
Example (Node.js)
const crypto = require("crypto");
const verifyWebhook = (req, signingSecret) => {
const signature = req.headers["x-subscord-signature"];
const timestamp = req.headers["x-subscord-timestamp"];
const body = JSON.stringify(req.body);
const expected = crypto
.createHmac("sha256", signingSecret)
.update(`${timestamp}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
};Example (Python)
import hmac
import hashlib
def verify_webhook(headers, body, signing_secret):
signature = headers.get("X-Subscord-Signature")
timestamp = headers.get("X-Subscord-Timestamp")
expected = hmac.new(
signing_secret.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Security Tip: Always use constant-time comparison functions (like
crypto.timingSafeEqualin Node.js orhmac.compare_digestin Python) to prevent timing attacks.
Payload Structure
All webhook payloads follow a consistent JSON structure:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "subscription.created",
"guildId": "123456789012345678",
"timestamp": 1736784000,
"data": {
"customer": {
"id": "987654321098765432",
"username": "johndoe",
"email": "[email protected]"
},
"product": {
"id": 1,
"name": "Premium"
},
"productOption": {
"id": 1,
"name": "Monthly",
"price": 9.99,
"currency": "USD",
"isFreeTrialOption": false,
"requiresCardOnTrial": false
},
"subscription": {
"id": 1,
"startedAt": 1736784000,
"expiresAt": 1739462400,
"active": true
},
"invoice": {
"id": "inv_abc123",
"amount": 9.99,
"currency": "USD",
"paymentMethod": "stripe"
}
}
}Field Reference
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the event (UUID) |
type | string | The event type (see Supported Events) |
guildId | string | The Discord server (guild) ID |
timestamp | number | Unix timestamp (seconds) when the event occurred |
data.customer.id | string | The customer's Discord user ID |
data.customer.username | string | The customer's Discord username |
data.customer.email | string | The customer's email address |
data.product.id | number | The product ID in Subscord |
data.product.name | string | The product name |
data.productOption.id | number | The product option ID |
data.productOption.name | string | The product option name (e.g., "Monthly") |
data.productOption.price | number | The price of the product option |
data.productOption.currency | string | The currency code (e.g., "USD") |
data.productOption.isFreeTrialOption | boolean | true when the product option is a free trial, false otherwise |
data.productOption.requiresCardOnTrial | boolean | true when the free trial requires a card on file (and can later convert to a paid subscription), false otherwise |
data.subscription.id | number | The subscription ID in Subscord |
data.subscription.startedAt | number | Unix timestamp when the subscription started |
data.subscription.expiresAt | number | Unix timestamp when the subscription expires |
data.subscription.active | boolean | Whether the subscription is currently active |
data.invoice.id | string | The invoice or transaction ID |
data.invoice.amount | number | The amount charged |
data.invoice.currency | string | The currency of the charge |
data.invoice.paymentMethod | string | The payment method used (stripe or crypto) |
Note: Some fields may be absent depending on the event type. For example,
trial.convertedevents will not includeinvoicedata for the original trial period.
Distinguishing Free Trials from Paid Subscriptions
The data.productOption.isFreeTrialOption and data.productOption.requiresCardOnTrial fields let you determine whether a webhook event is related to a free trial or a normal paid subscription.
Normal paid subscription:
{
"productOption": {
"id": 1,
"name": "Monthly",
"price": 9.99,
"currency": "USD",
"isFreeTrialOption": false,
"requiresCardOnTrial": false
}
}Both fields are false — the customer is on a standard billing cycle.
Free trial (no card required):
{
"productOption": {
"id": 2,
"name": "7-Day Free Trial",
"price": 0,
"currency": "USD",
"isFreeTrialOption": true,
"requiresCardOnTrial": false
}
}isFreeTrialOption is true and requiresCardOnTrial is false — the customer started a trial without providing payment details.
Free trial (card required):
{
"productOption": {
"id": 3,
"name": "14-Day Free Trial",
"price": 0,
"currency": "USD",
"isFreeTrialOption": true,
"requiresCardOnTrial": true
}
}isFreeTrialOption is true and requiresCardOnTrial is also true — the customer entered a card to start the trial. When the trial period ends, it can automatically convert to a paid subscription (triggering a trial.converted event).
Quick reference:
| Scenario | isFreeTrialOption | requiresCardOnTrial |
|---|---|---|
| Normal paid subscription | false | false |
| Free trial without card | true | false |
| Free trial with card | true | true |
Use these fields to route webhook events appropriately — for example, skipping invoice generation for trials, or flagging card-required trials for follow-up if they don't convert.
Testing Your Webhook
After configuring your endpoint, you can verify that everything is working correctly by using the Test Webhook button in your dashboard. This will send a sample subscription.created event to your endpoint so you can confirm that:
- Your server receives the request.
- The signature verification passes.
- Your integration processes the payload correctly.
Managing Your Webhook
From the Webhooks section in your dashboard settings, you can:
- Enable / Disable your webhook endpoint without deleting it.
- Edit the endpoint URL or name.
- Regenerate the signing secret if needed.
- Delete the webhook endpoint entirely.
Best Practices
- Respond quickly. Your endpoint should return a
2xxstatus code as fast as possible. Perform any heavy processing asynchronously after acknowledging receipt. - Verify signatures. Always validate the
X-Subscord-Signatureheader before processing the payload to ensure the request is authentic. - Use HTTPS. Your endpoint must use HTTPS to protect the payload data in transit.
- Handle duplicates. While unlikely, it's good practice to use the event
idfield to detect and ignore duplicate deliveries. - Store the raw body. Parse the raw request body as a string for signature verification before deserializing the JSON.
Note: In the current version, webhook deliveries are fire-and-forget. Failed deliveries are logged on Subscord's side but are not automatically retried. Make sure your endpoint is reliable and available.