> For the complete documentation index, see [llms.txt](https://developer.paddle.com/llms.txt).

# Quickstart

Everything you need to receive your first webhook from Paddle.

---

You can use webhooks to react to events that happen in your Paddle account, like a new subscription or a renewal. Paddle sends events to a URL you provide, so you can build your own logic to handle them.

This quickstart walks you through forwarding webhooks to a local server, creating a destination, and verifying signatures. It takes about five minutes.

## Before you begin

You need a Paddle account. You can sign up for free:

- [**Sandbox account**](https://sandbox-login.paddle.com/signup) — for testing. No real money is involved.
- [**Live account**](https://login.paddle.com/signup) — for production use. Requires approval before you can process real transactions.

We recommend starting in the sandbox environment.

You'll also need [Node.js](https://nodejs.org) installed locally to run the example handler.

## Use an AI agent

## Install the Hookdeck CLI {% step=true %}

The [Hookdeck CLI](https://hookdeck.com/docs/cli) gives your computer a public URL and forwards incoming webhooks to a port on `localhost`. It's free for development and the fastest way to receive real webhooks without deploying anything.

Install using your package manager:

{% code-group sync="js-package-manager" %}

```bash {% title="pnpm" %}
pnpm add -g hookdeck-cli
```

```bash {% title="yarn" %}
yarn global add hookdeck-cli
```

```bash {% title="npm" %}
npm install hookdeck-cli -g
```

{% /code-group %}

Or use Homebrew or Scoop:

{% code-group %}

```bash {% title="Homebrew (macOS)" %}
brew install hookdeck/hookdeck/hookdeck
```

```bash {% title="Scoop (Windows)" %}
scoop bucket add hookdeck https://github.com/hookdeck/scoop-hookdeck-cli.git
scoop install hookdeck
```

{% /code-group %}

[Visit hookdeck.com/docs to learn more about the Hookdeck CLI](<https://hookdeck.com/docs/cli#install>)

## Spin up a local listener {% step=true %}

Create a minimal webhook handler that accepts POST requests, logs the raw body, and responds with `200 OK`. Paddle retries on non-2xx responses, so returning a 2xx quickly is important.

```js {% title="server.js" %}
import express from "express";

const app = express();

app.post(
  "/webhooks",
  express.raw({ type: "application/json" }),
  (req, res) => {
    console.log("Received webhook:", req.body.toString());
    res.status(200).send("ok");
  }
);

app.listen(3000, () => console.log("Listening on :3000"));
```

Then, run it:

```bash
node server.js
```

In a second terminal, start Hookdeck and point it at port 3000:

```bash
hookdeck listen 3000 paddle-quickstart --path /webhooks
```

Hookdeck prints a public URL like `https://hkdk.events/abc123xyz`. Copy it for the next step.

{% callout type="info" %}
Keep both terminals open for the rest of the quickstart. If you stop the Hookdeck CLI, the public URL stops forwarding and webhooks will queue up rather than reach your server.
{% /callout %}

## Create a notification destination {% step=true %}

A [notification destination](https://developer.paddle.com/webhooks/about/notification-destinations.md) tells Paddle which events you want and where to send them.

You can create a notification destination using the API, dashboard, or MCP server. We'll use the dashboard for this quickstart.

{% instruction-steps %}

1. Go to **Paddle > Developer tools > Notifications**.
2. Click {% mock-button icon="carbon:add" %}New destination.
3. Set **Notification type** to **URL** and paste the Hookdeck URL from the previous step.
4. Choose the events you want to receive — for testing, `customer.created` is a good start.
5. Click Save destination.
6. Open the destination you just created and copy its **Secret key**. You'll need it for signature verification.

{% /instruction-steps %}

{% /dashboard-instructions %}

{% callout type="warning" %}
Treat your endpoint secret key like a password. Keep it safe and never share it with apps or people you don't trust.
{% /callout %}

## Send a test webhook {% step=true %}

Use [webhook simulator](https://developer.paddle.com/webhooks/simulator.md) to send a test webhook to your destination without having to take any real action, like completing a checkout.

{% instruction-steps %}

1. Go to **Paddle > Developer tools > Notifications** and click the **Simulations** tab.
2. Click {% mock-button icon="carbon:add" %}New simulation.
3. Choose your notification destination, select the `customer.created` event, and click Save.
4. Click Run on your simulation.

{% /instruction-steps %}

{% /dashboard-instructions %}

Within a second or two, you should see the event in the Hookdeck terminal and the parsed payload logged by your Node server. If nothing arrives, check that Hookdeck is still running and that the destination URL matches the one it printed.

## Verify the signature {% step=true %}

Every webhook Paddle sends includes a `Paddle-Signature` header. Verifying it on your side proves the request really came from Paddle — not from someone who found your public URL.

The quickest way to verify is with one of our SDKs. Install the Node.js SDK:

{% code-group sync="js-package-manager" %}

```bash {% title="pnpm" %}
pnpm add @paddle/paddle-node-sdk
```

```bash {% title="yarn" %}
yarn add @paddle/paddle-node-sdk
```

```bash {% title="npm" %}
npm install @paddle/paddle-node-sdk
```

{% /code-group %}

Replace your handler with a version that uses the SDK to verify before acting on the event:

```ts
import { Paddle, EventName } from "@paddle/paddle-node-sdk";
import express, { Request, Response } from "express";

const paddle = new Paddle("API_KEY");
const app = express();

// Create a `POST` endpoint to accept webhooks sent by Paddle.
// We need `raw` request body to validate the integrity. Use express raw middleware to ensure express doesn't convert the request body to JSON.
app.post(
  "/webhooks",
  express.raw({ type: "application/json" }),
  async (req: Request, res: Response) => {
    const signature = (req.headers["paddle-signature"] as string) || "";
    // req.body should be of type `buffer`, convert to string before passing it to `unmarshal`.
    // If express returned a JSON, remove any other middleware that might have processed raw request to object
    const rawRequestBody = req.body.toString();
    // Replace `WEBHOOK_SECRET_KEY` with the secret key in notifications from vendor dashboard
    const secretKey = process.env["WEBHOOK_SECRET_KEY"] || "";

    try {
      if (signature && rawRequestBody) {
        // The `unmarshal` function will validate the integrity of the webhook and return an entity
        const eventData = await paddle.webhooks.unmarshal(
          rawRequestBody,
          secretKey,
          signature,
        );
        switch (eventData.eventType) {
          case EventName.ProductUpdated:
            console.log(`Product ${eventData.data.id} was updated`);
            break;
          case EventName.SubscriptionUpdated:
            console.log(`Subscription ${eventData.data.id} was updated`);
            break;
          default:
            console.log(eventData.eventType);
        }
      } else {
        console.log("Signature missing in header");
      }
    } catch (e) {
      // Handle signature mismatch or other runtime errors
      console.log(e);
    }
    // Return a response to acknowledge
    res.send("Processed webhook event");
  },
);

app.listen(3000);
```

Set `PADDLE_API_KEY` and `WEBHOOK_SECRET_KEY` in your environment variables. Restart your server and run the simulation again. You should now see the verified event type logged.

For all supported languages and manual verification, see [Verify webhook signatures](https://developer.paddle.com/webhooks/about/signature-verification.md).

## Next steps

You're set up to receive verified webhooks. These pages cover what to build next:

{% card-group cols=3 %}

{% card title="How webhooks work" %}
Learn how Paddle delivers events, the [webhook lifecycle](https://developer.paddle.com/webhooks/about/how-webhooks-work.md), and delivery guarantees.
{% /card %}

{% card title="Respond to webhooks" %}
Best practices for [responding to webhooks](https://developer.paddle.com/webhooks/about/respond-to-webhooks.md), including retries and idempotency.
{% /card %}

{% card title="Signature verification" %}
See [verification examples](https://developer.paddle.com/webhooks/about/signature-verification.md) for Go, PHP, Python, Ruby, Java, and more.
{% /card %}

{% card title="Notification destinations" %}
Manage destinations, rotate secret keys, and [subscribe to more events](https://developer.paddle.com/webhooks/about/notification-destinations.md).
{% /card %}

{% card title="Webhook simulator" %}
Use [the simulator](https://developer.paddle.com/webhooks/simulator.md) to test lifecycle scenarios like subscription renewals and failed payments.
{% /card %}

{% card title="Event reference" %}
Browse the full [event catalog](https://developer.paddle.com/webhooks/transactions.md) grouped by entity to see every event Paddle sends.
{% /card %}

{% /card-group %}