Webhooks

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:

EventDescription
subscription.createdA new customer subscribes to a product
subscription.renewedAn existing subscription is renewed (crypto or Stripe auto-renewal)
subscription.expiredA subscription has expired and access has been removed
trial.convertedA free trial has been converted to a paid subscription
transaction.completedA payment has been successfully processed

Setting Up a Webhook

To configure a webhook for your server:

  1. Navigate to the Settings page in your Subscord Dashboard.
  2. Scroll to the Webhooks section.
  3. Click the Add Webhook button.
  4. Enter your Endpoint URL — this must be a valid HTTPS URL.
  5. Optionally, give your endpoint a Name (e.g., "My CRM Integration") for easy identification.
  6. 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:

HeaderDescription
X-Subscord-SignatureHMAC-SHA256 signature of the payload
X-Subscord-TimestampUnix timestamp (seconds) when the event was sent
X-Subscord-EventThe event type (e.g., subscription.created)

Signature Verification

The signature is computed using the following formula:

HMAC-SHA256(timestamp + "." + payload, signingSecret)

Where:

  • timestamp is the value from the X-Subscord-Timestamp header.
  • payload is the raw JSON request body.
  • signingSecret is your endpoint's signing secret.

To verify a webhook request:

  1. Extract the X-Subscord-Timestamp and X-Subscord-Signature headers.
  2. Read the raw request body as a string.
  3. Concatenate the timestamp, a period (.), and the raw body.
  4. Compute the HMAC-SHA256 hash of this string using your signing secret.
  5. 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.timingSafeEqual in Node.js or hmac.compare_digest in 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

FieldTypeDescription
idstringUnique identifier for the event (UUID)
typestringThe event type (see Supported Events)
guildIdstringThe Discord server (guild) ID
timestampnumberUnix timestamp (seconds) when the event occurred
data.customer.idstringThe customer's Discord user ID
data.customer.usernamestringThe customer's Discord username
data.customer.emailstringThe customer's email address
data.product.idnumberThe product ID in Subscord
data.product.namestringThe product name
data.productOption.idnumberThe product option ID
data.productOption.namestringThe product option name (e.g., "Monthly")
data.productOption.pricenumberThe price of the product option
data.productOption.currencystringThe currency code (e.g., "USD")
data.productOption.isFreeTrialOptionbooleantrue when the product option is a free trial, false otherwise
data.productOption.requiresCardOnTrialbooleantrue when the free trial requires a card on file (and can later convert to a paid subscription), false otherwise
data.subscription.idnumberThe subscription ID in Subscord
data.subscription.startedAtnumberUnix timestamp when the subscription started
data.subscription.expiresAtnumberUnix timestamp when the subscription expires
data.subscription.activebooleanWhether the subscription is currently active
data.invoice.idstringThe invoice or transaction ID
data.invoice.amountnumberThe amount charged
data.invoice.currencystringThe currency of the charge
data.invoice.paymentMethodstringThe payment method used (stripe or crypto)

Note: Some fields may be absent depending on the event type. For example, trial.converted events will not include invoice data 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:

ScenarioisFreeTrialOptionrequiresCardOnTrial
Normal paid subscriptionfalsefalse
Free trial without cardtruefalse
Free trial with cardtruetrue

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 2xx status code as fast as possible. Perform any heavy processing asynchronously after acknowledging receipt.
  • Verify signatures. Always validate the X-Subscord-Signature header 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 id field 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.