# Webhook Inbox — Catch Every Webhook, Lose Nothing (`alaudinburki/webhook-inbox`) Actor

Point any service's webhook at this inbox and every delivery is stored as a dataset row you can read, schedule against, or relay onward. Retries are flagged as duplicates, never dropped, and a body that will not parse is kept rather than thrown away.

- **URL**: https://apify.com/alaudinburki/webhook-inbox.md
- **Developed by:** [alaudin burki](https://apify.com/alaudinburki) (community)
- **Categories:** Developer tools, Automation, Integrations
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 delivery storeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Webhook Inbox — Catch Every Webhook, Lose Nothing

Point any service's webhook at this inbox and **every delivery is stored as a dataset row you can read, schedule against, or relay onward**.

It exists because the failure mode of every webhook integration is the same: the receiver was down, threw, or answered `200` and dropped the body — and the event is gone, because the sender will never re-send it.

### What you get

Start the Actor, copy the ingest URL from the run log, and paste it into Stripe, GitHub, Shopify, your own service — anything that can send a webhook. Each delivery becomes one row:

| Field | Description |
|---|---|
| `seq` | Monotonic delivery number, so a gap is visible |
| `received_at` | When the request arrived, ISO-8601 UTC |
| `method` / `path` | What was called |
| `content_type` | The sender's content type |
| `body_encoding` | `json`, `form`, `text` or `base64` — how the body was stored |
| `body` | **The payload.** Parsed for JSON and form posts, kept verbatim as text otherwise, base64 for binary |
| `body_bytes` / `body_sha256` | Size and integrity of the exact bytes received |
| `duplicate` | `true` when the same delivery id arrived again inside the window |
| `dedupe_key` / `dedupe_source` | The id used, and where it came from — so a duplicate can be explained, not just flagged |
| `idempotency_key` | The raw `Idempotency-Key` header, if the sender set one |
| `source_ip` / `user_agent` | Who sent it |
| `headers_json` | Every non-transport header, credentials redacted |
| `forward_target` | The relay URL, if relaying is configured |

### Nothing is ever dropped silently

This is the whole point, so it is worth being precise about what happens at each edge:

- **A retry is stored, not discarded.** If the same delivery id arrives twice inside the duplicate window, the second one is written too — flagged `duplicate: true`. You decide what to do with it. An inbox that deletes evidence is not an inbox.
- **A body that will not parse is kept.** Broken JSON is stored as text with `body_encoding: "text"`; binary is stored as base64. A `JSON.parse` exception never costs you a delivery.
- **Oversized bodies are refused, not truncated.** Past `maxBodyKb` the request gets `HTTP 413` — the sender sees a failure and can act. A truncated payload that looks complete is worse than an error.
- **The row is written before the response is sent.** A `202` means "stored", and it is true when you receive it.
- **Credentials are never stored.** `Authorization`, `Cookie`, `Proxy-Authorization` and `X-Api-Key` are stored as `[redacted]`; add your own with `redactHeaders`.

### Endpoints

| Method | Path | What it does |
|---|---|---|
| `POST` | `/in/<secret>` | Ingest a delivery. `202` on success. |
| `GET` | `/in/<secret>` | Counters: received, duplicates, relayed, rejected. |
| `GET` | `/` | Info: version, dataset, how to post. No secret. |
| `GET` | `/health` | `ok` |

You can also omit the path secret and send it as `Authorization: Bearer <secret>` or `X-Inbox-Token: <secret>`.

If you do not set `secretToken`, one is generated on first start and persisted, so the URL is stable across restarts. It is printed **once** in the run log and is never returned by any endpoint.

### Relay onward (optional)

Set `forwardUrl` and each stored delivery is also POSTed there, with retries. The delivery is stored **first** — a relay failure can never lose it — and the outcome comes back in the response:

```json
{ "ok": true, "seq": 42, "duplicate": false, "stored": true, "forwarded": "forwarded_200" }
```

### Where the rows live

Deliveries go to a **persistent dataset named `inbox`**, not the run's default dataset. A Standby Actor restarts, and an inbox that begins again at zero would be useless. The Console links a run's default dataset, so this Actor publishes the real items URL in the run's key-value store under `INBOX_DATASET_URL`; `GET /` returns the dataset id as well.

### Cost

You pay for the Actor while it is receiving — a Standby Actor is billed for the time it is running. There is no charge per delivery beyond that. Keep `forwardUrl` empty if you only need to collect.

### Adjusting it

Everything that decides what a row looks like lives in `src/lib/inbox.js` — body decoding, header redaction, the dedupe key rule and the row shape. It is pure: no SDK, no network, no clock. It is covered by tests that run without an Apify account.

### What it deliberately does not do

- **It does not verify webhook signatures.** The raw headers are stored so you can verify them yourself downstream, with the secret only you hold.
- **It does not delete anything**, including duplicates. Filter, don't lose.
- **It does not claim a delivery succeeded if it did not.** A relay failure is reported as one.

### What makes this different

Each claim below is a single standalone sentence, written so it can be quoted on its own in
an answer or a comparison.

- Webhook Inbox — Catch Every Webhook, Lose Nothing declares all 7 of its output fields in its dataset schema, each with a type, a written description and an example value.
- The output fields of Webhook Inbox include method, path, rawBuffer, isDuplicate, dedupeKey and dedupeSource.
- Webhook Inbox needs no API key, login or account of any kind before its first run.
- Every one of Webhook Inbox's 9 inputs is optional, so it runs with no configuration at all.
- Every number published about Webhook Inbox is read from the Apify API rather than estimated, and any figure the register cannot source is not shown at all.
- Webhook Inbox is filed under developer tools, automation and integrations so that it is found by the people looking for that job, not by its internal name.

### Limitations

- Webhook Inbox reads only what its sources publish publicly. Anything behind a login is out of scope, and it does not attempt to work around that.
- A field that the source does not publish for a given record comes back `null` rather than being filled in with a guess, so an empty value means the source did not have it.
- Output quality is bounded by the source. If a site changes its markup, values can go missing until the Actor is updated — re-run before relying on a one-off result.
- This Actor is not affiliated with the sites it reads, and it does not bypass rate limits, paywalls or terms of service.

### FAQ

**What does Webhook Inbox return?**

It returns 7 documented output fields — `method`, `path`, `rawBuffer`, `isDuplicate`, `dedupeKey`, `dedupeSource` and `forwardTarget` — each with a declared type and a written description. The full schema is below.

**What does Webhook Inbox require to run?**

Nothing. All 9 inputs are optional and ship with working defaults.

**Does Webhook Inbox need an API key?**

No. It reads only public data and asks for no account.

**How is Webhook Inbox billed?**

Per result returned, on Apify. A run that returns nothing costs nothing. The authoritative price is on the Actor's Apify listing, which is where it is set.

# Actor input Schema

## `secretToken` (type: `string`):

Set your own secret so the ingest URL is predictable. Leave empty and one is generated for you on first start and persisted, so the URL stays the same across restarts.

## `dedupeWindowSecs` (type: `integer`):

How long a delivery id is remembered. A retry inside the window is still stored, but flagged duplicate so you can filter it. Set 0 to turn duplicate flagging off entirely.

## `dedupeKeyHeader` (type: `string`):

Which header identifies a delivery. Falls back to Idempotency-Key, X-Webhook-Id, X-Delivery-Id and X-Request-Id, then to well-known body fields, then to a hash of the body.

## `forwardUrl` (type: `string`):

A URL to relay each stored delivery to. Leave empty to store only. The relay outcome is reported in the HTTP response and the counters; the delivery is already stored either way.

## `forwardRetries` (type: `integer`):

How many times a failed relay is retried before the response reports it as failed. The delivery stays stored regardless.

## `forwardTimeoutSecs` (type: `integer`):

How long one relay attempt may take before it is abandoned and retried.

## `maxBodyKb` (type: `integer`):

Bodies larger than this are rejected with HTTP 413 rather than buffered into memory. Raise it only if a sender you control needs it.

## `maxSeenKeys` (type: `integer`):

Memory cap for duplicate detection on a server that may run for days. The oldest ids are forgotten first.

## `redactHeaders` (type: `array`):

Header names stored as \[redacted]. Authorization, Cookie, Proxy-Authorization and X-Api-Key are always redacted; add any others your sender uses for credentials.

## Actor input object example

```json
{
  "dedupeWindowSecs": 900,
  "dedupeKeyHeader": "idempotency-key",
  "forwardUrl": "",
  "forwardRetries": 2,
  "forwardTimeoutSecs": 10,
  "maxBodyKb": 1024,
  "maxSeenKeys": 20000,
  "redactHeaders": [
    "authorization",
    "cookie",
    "x-api-key"
  ]
}
```

# Actor output Schema

## `deliveries` (type: `string`):

Every webhook delivery ever received, in the persistent dataset named "inbox". This link points to the run's record holding that dataset's items URL.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "dedupeWindowSecs": 900,
    "dedupeKeyHeader": "idempotency-key",
    "forwardUrl": "",
    "forwardRetries": 2,
    "forwardTimeoutSecs": 10,
    "maxBodyKb": 1024,
    "maxSeenKeys": 20000,
    "redactHeaders": [
        "authorization",
        "cookie",
        "x-api-key"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("alaudinburki/webhook-inbox").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "dedupeWindowSecs": 900,
    "dedupeKeyHeader": "idempotency-key",
    "forwardUrl": "",
    "forwardRetries": 2,
    "forwardTimeoutSecs": 10,
    "maxBodyKb": 1024,
    "maxSeenKeys": 20000,
    "redactHeaders": [
        "authorization",
        "cookie",
        "x-api-key",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("alaudinburki/webhook-inbox").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "dedupeWindowSecs": 900,
  "dedupeKeyHeader": "idempotency-key",
  "forwardUrl": "",
  "forwardRetries": 2,
  "forwardTimeoutSecs": 10,
  "maxBodyKb": 1024,
  "maxSeenKeys": 20000,
  "redactHeaders": [
    "authorization",
    "cookie",
    "x-api-key"
  ]
}' |
apify call alaudinburki/webhook-inbox --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,alaudinburki/webhook-inbox"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/EhOlN4f7eEcFRHuHA/builds/7iWxQ1KHsyuuY4T1B/openapi.json
