Features

Webhooks

Get notified when things happen with webhooks.

Webhooks

Webhooks allow you to build or set up integrations, such as Make or Pabbly workflows or Slack notifications, when certain events occur in your CheckoutJoy account.

When one of those events is triggered, we'll send a HTTP POST payload to the webhook's configured URL.

Creating a webhook

Webhooks are configured in Settings > Webhooks in your CheckoutJoy Dashboard.

Webhook Security

To verify that a webhook was sent by CheckoutJoy, you can check the request signature that is sent with each webhook.

The signature is sent in a HTTP header named x-cj-signature, and it's an HMAC hash of the request payload hashed using your secret key.

To verify the signature, you need to:

  1. Get the x-cj-signature header from the webhook request
  2. Hash the request payload using sha256 and your secret key
  3. Compare the hash with the signature from x-cj-signature

Here is an example of how to verify the signature in your app:

Verifying a request


const signature = req.headers['x-cj-signature']
const hash = require("crypto")
                 .createHmac("sha256", SECRET_KEY)
                 .update(JSON.stringify(req.body))
                 .digest("hex")

if (hash === signature) {
  // Request is verified
} else {
  // Request could not be verified
}

Events

Purchase Completed

The purchase.completed event is triggered when an order is completed. The order is completed and product access can be granted to the customer.

{
  "event": "purchase.completed",
  "data": {
    "id": "1685958796591237c8f84-7c5a-405e-8961-02a691deffc7",
    "affiliate_code": null,
    "status": "COMPLETED",
    "country_code": "ZA",
    "completed_at": "2023-06-05T09:53:34.991Z",
    "customer": {
      "email": "mollie@checkoutjoy.com",
      "first_name": "Koos",
      "last_name": "Mollie",
      "billing_address": {
        "first_name": "Koos",
        "last_name": "Mollie"
      }
    },
    "products": [
      {
        "id": "27bPgEHKrwtHlaP2JsyNt9rb6z9",
        "name": "My First Digital Product",
        "total": 21.8,
        "sub_total": 20,
        "tax_total": 1.8,
        "discount_total": 0,
        "charge_type": "immediate",
        "price": {
          "id": "price_id",
          "currency": "USD",
          "amount": 20,
          "billing_type": "once",
          "recurring": {
            "type": "once",
            "interval": null,
            "period": "month",
            "cycles": null,
            "trial_period_days": null
          }
        },
        "tax": {
          "name": "BTW",
          "rate": 9,
          "region": null,
          "country": "NL",
          "tax_class": "reduced",
          "behaviour": "exclusive"
        }
      }
    ],
    "ref": "000186",
    "payment": {
      "id": "1aceda64-5a4b-4741-a2b0-4c933a600b09",
      "status": "COMPLETED",
      "currency": "USD",
      "amount": 21.8,
      "amount_fee": 0,
      "total_net": 21.8,
      "processor_id": "123456789",
      "created_at": "2023-06-05T09:53:29.086Z"
    },
    "processor_id": "Stripe",
    "currency": "USD",
    "total": 21.8,
    "discount_total": 0,
    "tax_total": 1.8
  }
}

When does purchase.completed fire?

purchase.completed fires only when the underlying payment processor has confirmed the payment is complete. If an order is still in PENDING status (because the processor webhook for "payment completed" hasn't arrived yet — see the Klarna and Vipps notes for examples), no purchase.completed event is sent. Downstream automations should treat the presence of this event as the authoritative "deliver the goods now" signal.

Custom fields on the payload

If you've added custom fields to the checkout (text inputs, selects, or checkboxes), the values the buyer entered will be included in the purchase.completed payload under the variable names you configured. You control the variable name per field — give your custom fields names that match what your downstream system expects (for example country_of_residence, buyer_type, vat_id) and your integration code doesn't need a separate mapping step.

Consent checkboxes also appear here — the buyer's actual value (true/false) and the exact wording they saw at the time of purchase are stored with the order, which makes the payload useful as legal/audit evidence for things like the EU digital-content withdrawal-right consents.

Recurring Payment Received

This event is triggered when a recurring subscription payment is processed and completed.

{
  "event": "subscription.payment_received",
  "data": {
    "purchase_id": "1685958796591237c8f84-7c5a-405e-8961-02a691deffc7",
    "id": "1aceda64-5a4b-4741-a2b0-4c933a600b09",
    "status": "COMPLETED",
    "currency": "USD",
    "amount": 21.8,
    "amount_fee": 0,
    "amount_net": 21.8,
    "created_at": "2023-06-05T09:53:29.086Z",
    "processor_id": "pi_3NFa1IIocmTNbZyh2aEImwUE",
    "processor": "Stripe"
  }
}

Subscription Cancelled

The subscription.cancelled event is triggered when a payment subscription failed or is cancelled by the merchant or the customer.

A cancelled subscription is final and can’t be fixed or restarted.

All product access must be revoked when this event is received.

{
  "event": "purchase.completed",
  "data": {
    "id": "1685958796591237c8f84-7c5a-405e-8961-02a691deffc7",
    "affiliate_code": null,
    "status": "CANCELLED",
    "country_code": "ZA",
    "completed_at": "2023-06-05T09:53:34.991Z",
    "customer": {
      "email": "mollie@checkoutjoy.com",
      "first_name": "Koos",
      "last_name": "Koekies",
      "billing_address": {
        "first_name": "Koos",
        "last_name": "Mollie"
      }
    },
    "products": [
      {
        "id": "27bPgEHKrwtHlaP2JsyNt9rb6z9",
        "name": "My First Digital Product",
        "total": 21.8,
        "sub_total": 20,
        "tax_total": 1.8,
        "discount_total": 0,
        "charge_type": "immediate",
        "price": {
          "id": "price_id",
          "currency": "USD",
          "amount": 20,
          "billing_type": "once",
          "recurring": {
            "type": "once",
            "interval": null,
            "period": "month",
            "cycles": null,
            "trial_period_days": null
          }
        },
        "tax": {
          "name": "BTW",
          "rate": 9,
          "region": null,
          "country": "NL",
          "tax_class": "reduced",
          "behaviour": "exclusive"
        }
      }
    ],
    "ref": "000186",
    "payment": {
      "id": "1aceda64-5a4b-4741-a2b0-4c933a600b09",
      "status": "COMPLETED",
      "currency": "USD",
      "amount": 21.8,
      "amount_fee": 0,
      "total_net": 21.8,
      "processor_id": "123456789",
      "created_at": "2023-06-05T09:53:29.086Z"
    },
    "processor_id": "Stripe",
    "currency": "USD",
    "total": 21.8,
    "discount_total": 0,
    "tax_total": 1.8
  }
}
Webhooks