> ## Documentation Index
> Fetch the complete documentation index at: https://docs.posthook.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Receiving Webhooks

> Handle webhook deliveries from Posthook in your application

When the scheduled time arrives, Posthook sends an HTTP POST request to your endpoint. This page covers the request format, how to handle deliveries, and production best practices.

The destination URL combines your **Project Domain** (configured in [settings](/essentials/project-settings)) with the **Path** specified when scheduling the hook.

> `api.myapp.com` (Domain) + `/webhooks/remind` (Path) = `https://api.myapp.com/webhooks/remind` (HTTPS is enforced)

## The Request

Posthook sends a `POST` request containing your data along with scheduling metadata.

```json theme={null}
{
  "id": "e5405623-2c1c-460e-9737-c884f7f59035",
  "path": "/webhooks/remind",
  "postAt": "2025-01-01T12:00:00Z",
  "postedAt": "2025-01-01T12:00:00.123Z",
  "data": {
    "user_id": "usr_123",
    "reminder_id": "rem_456"
  },
  "createdAt": "2024-12-31T12:00:00Z",
  "updatedAt": "2024-12-31T12:00:00Z"
}
```

### Headers

| Header               | Description                                                                                                         |
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`       | `application/json`                                                                                                  |
| `Posthook-Id`        | Unique hook ID (e.g. `e5405623-2c1c-460e-9737-c884f7f59035`)                                                        |
| `Posthook-Timestamp` | Unix timestamp (seconds) of when the hook was posted                                                                |
| `Posthook-Signature` | One or more `v1,<hex>` signatures, space-separated                                                                  |
| `Posthook-Ack-URL`   | Async ack callback URL ([async hooks](/essentials/async-hooks))                                                     |
| `Posthook-Nack-URL`  | Async nack callback URL ([async hooks](/essentials/async-hooks))                                                    |
| `X-Ph-Signature`     | Legacy HMAC-SHA256 signature (hex-encoded). See [Legacy Header](/essentials/security#legacy-header-x-ph-signature). |
| `User-Agent`         | `Posthook/1.0`                                                                                                      |

## Framework Examples

Complete webhook handlers showing how to receive a delivery, verify the signature, and return a response. Each example uses the official SDK for [signature verification](/essentials/security).

<CodeGroup>
  ```javascript Express.js theme={null}
  import express from 'express';
  import { Signatures, SignatureVerificationError } from '@posthook/node';

  const app = express();
  app.use(express.json());

  const signatures = new Signatures(process.env.POSTHOOK_SIGNING_KEY);

  app.post('/webhooks/posthook', async (req, res) => {
    try {
      const delivery = signatures.parseDelivery(req.body, req.headers);

      // Your business logic
      const { user_id, reminder_id } = delivery.data;
      await sendReminder(user_id, reminder_id);

      res.status(200).send('OK');
    } catch (err) {
      if (err instanceof SignatureVerificationError) {
        return res.status(401).json({ error: err.message });
      }
      throw err;
    }
  });
  ```

  ```javascript Next.js (App Router) theme={null}
  import { Signatures, SignatureVerificationError } from '@posthook/node';
  import { NextResponse } from 'next/server';

  const signatures = new Signatures(process.env.POSTHOOK_SIGNING_KEY);

  export async function POST(request) {
    const body = await request.text();
    const headers = Object.fromEntries(request.headers);

    try {
      const delivery = signatures.parseDelivery(body, headers);

      // Your business logic
      const { user_id, reminder_id } = delivery.data;
      await sendReminder(user_id, reminder_id);

      return NextResponse.json({ ok: true });
    } catch (err) {
      if (err instanceof SignatureVerificationError) {
        return NextResponse.json({ error: err.message }, { status: 401 });
      }
      throw err;
    }
  }
  ```

  ```python Flask theme={null}
  from flask import Flask, request, jsonify
  from posthook import SignaturesService, SignatureVerificationError

  app = Flask(__name__)
  signatures = SignaturesService(os.environ["POSTHOOK_SIGNING_KEY"])

  @app.route('/webhooks/posthook', methods=['POST'])
  def handle_webhook():
      try:
          delivery = signatures.parse_delivery(
              request.get_data(),
              dict(request.headers)
          )

          # Your business logic
          send_reminder(delivery.data["user_id"], delivery.data["reminder_id"])

          return "OK", 200
      except SignatureVerificationError as err:
          return jsonify(error=err.message), 401
  ```

  ```python FastAPI theme={null}
  from fastapi import FastAPI, Request, HTTPException
  from posthook import SignaturesService, SignatureVerificationError

  app = FastAPI()
  signatures = SignaturesService(os.environ["POSTHOOK_SIGNING_KEY"])

  @app.post('/webhooks/posthook')
  async def handle_webhook(request: Request):
      body = await request.body()
      headers = dict(request.headers)

      try:
          delivery = signatures.parse_delivery(body, headers)

          # Your business logic
          await send_reminder(delivery.data["user_id"], delivery.data["reminder_id"])

          return {"ok": True}
      except SignatureVerificationError as err:
          raise HTTPException(status_code=401, detail=err.message)
  ```

  ```go Go (net/http) theme={null}
  package main

  import (
  	"io"
  	"net/http"
  	"os"

  	posthook "github.com/posthook/posthook-go"
  )

  var signatures, _ = posthook.NewSignatures(os.Getenv("POSTHOOK_SIGNING_KEY"))

  func handleWebhook(w http.ResponseWriter, r *http.Request) {
  	body, _ := io.ReadAll(r.Body)

  	delivery, err := signatures.ParseDelivery(body, r.Header)
  	if err != nil {
  		http.Error(w, "Invalid signature", http.StatusUnauthorized)
  		return
  	}

  	// delivery.Data (json.RawMessage) contains your verified payload
  	// delivery.HookID, delivery.Path, delivery.PostedAt also available
  	w.WriteHeader(http.StatusOK)
  }
  ```
</CodeGroup>

<Tip>
  For a deep dive on how signatures work, key rotation, and replay protection, see the [Security](/essentials/security) page.
</Tip>

## Retries

Posthook guarantees **at-least-once delivery**. If your endpoint returns a non-2xx status code or times out (10-second limit), Posthook retries the delivery based on your project's retry configuration.

You can configure:

* **Max Retries**: Up to 15 attempts (plan-dependent).
* **Retry Delay**: 5 to 60 seconds between attempts.

See [Project Settings](/essentials/project-settings) to configure defaults for your project.

### Per-hook retry override

Individual hooks can override your project's retry settings via the `retryOverride` field.

```json theme={null}
{
  "path": "/webhooks/process-payment",
  "postAt": "2026-03-14T18:00:00Z",
  "data": { "order_id": "ord_789" },
  "retryOverride": {
    "minRetries": 15,
    "delaySecs": 5,
    "strategy": "fixed"
  }
}
```

Choose `fixed` for constant delay or `exponential` for increasing delays with `backoffFactor`, `maxDelaySecs`, and `jitter`. See [Scheduling: Per-hook retry override](/essentials/scheduling#per-hook-retry-override) for the full field reference.

## Idempotency

Because at-least-once delivery can produce rare duplicates under failure conditions, your hook handler should be **idempotent**. The right approach varies by use case. Some operations are naturally idempotent (e.g., setting a value in a database). Others need explicit deduplication.

For cases that need it, include a unique ID in your payload (e.g., `idempotency_key`, `event_id`, or your own business logic ID like `reminder_id`). You can also use the `id` field from the hook delivery itself as your idempotency key.

```json theme={null}
{
  "path": "/webhooks/remind",
  "data": {
    "reminder_id": "rem_559201",
    "user_id": "usr_201"
  }
}
```

When your handler receives the request, use a transaction to atomically claim the ID and do the work. If the work fails, the transaction rolls back and the retry can try again.

<CodeGroup>
  ```javascript Node.js theme={null}
  // IDEMPOTENCY: claim the ID and do the work in one transaction
  app.post('/webhooks/posthook/remind', async (req, res) => {
    const { reminder_id, user_id } = req.body.data;
    const client = await pool.connect();

    try {
      await client.query('BEGIN');
      await client.query(
        'INSERT INTO processed_hooks (idempotency_key) VALUES ($1)',
        [reminder_id]
      );

      await sendReminder(user_id, reminder_id);

      await client.query('COMMIT');
    } catch (err) {
      await client.query('ROLLBACK');
      if (err.code === '23505') { // unique_violation: already processed
        return res.status(200).send('OK');
      }
      throw err;
    } finally {
      client.release();
    }

    res.status(200).send('OK');
  });
  ```

  ```python Python theme={null}
  # IDEMPOTENCY: claim the ID and do the work in one transaction
  @app.route('/webhooks/posthook/remind', methods=['POST'])
  def handle_remind():
      data = request.json['data']
      reminder_id = data['reminder_id']

      try:
          db.execute(
              'INSERT INTO processed_hooks (idempotency_key) VALUES (%s)',
              (reminder_id,)
          )
          send_reminder(data['user_id'], reminder_id)
          db.commit()
      except IntegrityError:
          db.rollback()
          return 'OK', 200

      return 'OK', 200
  ```

  ```go Go theme={null}
  // IDEMPOTENCY: claim the ID and do the work in one transaction
  func handleRemind(w http.ResponseWriter, r *http.Request) {
  	var hook struct {
  		Data struct {
  			ReminderID string `json:"reminder_id"`
  			UserID     string `json:"user_id"`
  		} `json:"data"`
  	}
  	json.NewDecoder(r.Body).Decode(&hook)

  	tx, err := db.Begin()
  	if err != nil {
  		http.Error(w, "Internal error", http.StatusInternalServerError)
  		return
  	}
  	defer tx.Rollback()

  	_, err = tx.Exec(
  		"INSERT INTO processed_hooks (idempotency_key) VALUES ($1)",
  		hook.Data.ReminderID,
  	)
  	if err != nil {
  		// Duplicate: already processed
  		w.WriteHeader(http.StatusOK)
  		return
  	}

  	sendReminder(hook.Data.UserID, hook.Data.ReminderID)

  	if err := tx.Commit(); err != nil {
  		http.Error(w, "Internal error", http.StatusInternalServerError)
  		return
  	}
  	w.WriteHeader(http.StatusOK)
  }
  ```
</CodeGroup>

If the work fails (e.g., `sendReminder` errors), the transaction rolls back, the idempotency key is not recorded, and the hook is retried.

<Tip>
  If you don't use a relational database, you can achieve the same with Redis `SET key NX EX ttl` combined with a cleanup step on failure. The key is that the lock should only persist once the work succeeds.
</Tip>

For more background on at-least-once delivery and idempotent design, see Twilio's [Exactly-once delivery](https://www.twilio.com/en-us/blog/insights/exactly-once-delivery) and Stripe's [Designing with Idempotency](https://stripe.com/blog/idempotency).

## Handling Long-Running Tasks

Posthook enforces a **10-second timeout** on webhook delivery. If your task takes longer than 10 seconds (e.g., generating a PDF, processing a video), your request will time out and be retried.

You have two options:

### Option 1: Async Hooks (Recommended)

Enable [Async Hooks](/essentials/async-hooks) on your project and return **HTTP 202**. Posthook will wait for your ack/nack callback instead of timing out. This gives you built-in timeout management, retry on failure, and visibility into async processing status in the dashboard.

### Option 2: Queue Locally

If you prefer to manage your own job queue:

1. Receive the webhook.
2. Push the job to an internal background queue (e.g., Redis, SQS, BullMQ).
3. **Return `200 OK` immediately** to Posthook.
4. Process the job asynchronously in your worker.

<Tip>
  If you need to track the completion of these long-running tasks (especially if they are 3rd-party APIs), check out the [Recursive Polling](/patterns/polling) pattern.
</Tip>
