# Guide to Amazon EventBridge Webhooks: Features and Best Practices

Amazon EventBridge is AWS's serverless event bus. AWS services, SaaS partners and your own applications put events on a bus, and rules route them to targets. When the target is your own HTTP service rather than a Lambda function or a queue, EventBridge calls it through an API destination, which is how EventBridge does webhooks.

This guide covers how API destinations and connections fit together, what the requests look like, how to authenticate them with a Basic connection, the timeout and retry rules you need to plan for, and the practices that keep an EventBridge receiver reliable in production.

## What are Amazon EventBridge webhooks?

An API destination is an HTTPS endpoint that EventBridge invokes as the target of an event bus rule, a pipe or a Custom Event Bus subscriber. When an event matches the rule's event pattern, EventBridge sends it to the URL with the HTTP method you chose for the destination.

Every API destination uses a connection, which holds the authorization method and credentials. Connections support three methods: Basic, API Key and OAuth Client Credentials. For Basic and API Key, EventBridge builds the authorization header itself from the values you stored, and for OAuth it exchanges a client ID and secret for an access token and manages the token. EventBridge keeps the credentials in AWS Secrets Manager.

There is no signature. EventBridge adds no HMAC header and no timestamp, and API destinations do not support mutual TLS. With a Basic connection, the `Authorization` header is the only thing that tells your endpoint a request came from your EventBridge account.

## Amazon EventBridge webhook features

| Feature | Details |
| --- | --- |
| Configuration | A connection, an API destination (URL, method, invocation rate) and a rule, pipe or Custom Event Bus subscriber that targets it |
| Authentication | Connection credentials: Basic, API Key or OAuth Client Credentials. No signature |
| HTTP method | Any except `CONNECT` and `TRACE`. `PUT` and `POST` are the most common |
| Body | The matched event envelope, or the output of an input transformer |
| Fixed headers | `User-Agent: Amazon/EventBridge/ApiDestinations`, `Content-Type: application/json; charset=utf-8` unless you set one, and `Range: bytes=0-1048575` |
| Timeout | 5 seconds |
| Retries | `401`, `407`, `409`, `429`, `5xx` and timeouts, honouring `Retry-After`. Event buses default to 24 hours and 185 attempts |
| After retries | The event goes to a dead-letter queue if one is configured, otherwise it is dropped |
| Rate limiting | An invocation rate per second that you set on the destination |
| Endpoint | HTTPS, public or private. No mTLS |
| Source IPs | Not published |

`User-Agent` and `Range` cannot be overridden, and EventBridge strips a list of other headers from every request, including `Origin`, `Referer` and `Forwarded`.

## Events and payloads

EventBridge has no webhook event list of its own. The API destination receives whatever the rule's event pattern matches, from any AWS service, SaaS partner or application that puts events on the bus.

Without an input transformer, the body is the EventBridge event envelope. Here is an EC2 state-change event:

```json
{
  "version": "0",
  "id": "7bf73129-1428-4cd3-a780-95db273d1602",
  "detail-type": "EC2 Instance State-change Notification",
  "source": "aws.ec2",
  "account": "123456789012",
  "time": "2019-10-02T17:40:17Z",
  "region": "us-east-1",
  "resources": ["arn:aws:ec2:us-east-1:123456789012:instance/i-abcd1111"],
  "detail": { "instance-id": "i-abcd1111", "state": "pending" }
}

```

Route on `source` and `detail-type` together. AWS service events have a `source` that starts with `aws.`, and your own custom events use whatever `source` and `detail-type` your application puts on the bus. The `id` is unique per event, which makes it the natural deduplication key.

An input transformer replaces the envelope with a template of your own, which can produce JSON or plain text. If you use one, your receiver parses whatever the template builds, and it only has a usable event ID if you include `id` in the template. A `GET` or `DELETE` destination normally sends no body at all.

> **Inspect EventBridge events as they arrive.** Point an API destination at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to see the exact headers and body EventBridge sends, with no account or setup required.

## Setting up Amazon EventBridge webhooks

You choose the credentials. Pick a username and generate a long random password, for example with `openssl rand -hex 32`, and store both on your receiver.

1. In the EventBridge console, open **API destinations**, select the **Connections** tab and click **Create connection**.
2. Name the connection, choose **Basic** as the authorization type, and enter the username and password. Your AWS account needs Secrets Manager permissions, which `AmazonEventBridgeFullAccess` includes.
3. Open the **API destinations** tab and click **Create API destination**. Enter your endpoint URL, choose the HTTP method, set an invocation rate limit if you need one, and select the connection.
4. Create a rule on your event bus with an event pattern, for example `{"source": ["aws.ec2"], "detail-type": ["EC2 Instance State-change Notification"]}`, and choose **EventBridge API destination** as the target. Add an input transformer only if your endpoint needs a different shape.
5. Configure the rule target's retry policy and, ideally, a dead-letter queue.

To put Hookdeck in front of your service, create a Hookdeck source with the **Amazon EventBridge** type and use its URL as the API destination endpoint. The source asks for the **Username** and **Password** you entered on the connection. It pre-selects `POST`. If your API destination uses `PUT`, `PATCH`, `GET` or `DELETE`, allow that method on the source too, or Hookdeck will not accept the requests.

For local development, `hookdeck listen 3000 aws-eventbridge --path /webhooks/aws-eventbridge` gives you a public HTTPS URL that forwards to your machine. EventBridge cannot reach `localhost` directly.

## Securing Amazon EventBridge webhooks

With a Basic connection, every request carries `Authorization: Basic base64(username:password)`. Decode it, split on the first colon (a password can contain colons, a username cannot), and compare the username and password with your stored values in constant time. Evaluate both comparisons before deciding, and reject everything if the credentials are not configured.

```javascript
const crypto = require('crypto');
const express = require('express');

function safeEqual(a, b) {
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifyBasicAuth(header, username, password) {
  if (!username || !password) return false; // never accept when unconfigured
  const match = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(header || '');
  if (!match) return false;
  const decoded = Buffer.from(match[1], 'base64');
  const colon = decoded.indexOf(0x3a); // split on the first colon: passwords may contain ':'
  if (colon === -1) return false;
  const userOk = safeEqual(decoded.subarray(0, colon), Buffer.from(username, 'utf8'));
  const passOk = safeEqual(decoded.subarray(colon + 1), Buffer.from(password, 'utf8'));
  return userOk && passOk; // evaluate both before deciding
}

const app = express();

// The API destination's method is configurable, so accept the ones you use.
app.all('/webhooks/aws-eventbridge', express.raw({ type: '*/*' }), (req, res) => {
  const ok = verifyBasicAuth(
    req.get('authorization'),
    process.env.AWS_EVENTBRIDGE_WEBHOOK_USERNAME,
    process.env.AWS_EVENTBRIDGE_WEBHOOK_PASSWORD
  );
  if (!ok) return res.sendStatus(401);

  const raw = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : '';
  let event = {};
  try {
    event = raw ? JSON.parse(raw) : {};
  } catch {
    // An input transformer can emit plain text. Handle it here if you use one.
  }

  // Route on event.source and event['detail-type']; deduplicate on event.id.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import binascii
import hmac
import os
import re

BASIC_PATTERN = re.compile(r"^basic\s+([A-Za-z0-9+/]+={0,2})\s*$", re.IGNORECASE)

def verify_basic_auth(header, username, password) -> bool:
    if not username or not password:
        return False  # never accept when unconfigured
    match = BASIC_PATTERN.match(header or "")
    if not match:
        return False
    try:
        decoded = base64.b64decode(match.group(1), validate=True)
    except (binascii.Error, ValueError):
        return False
    received_user, sep, received_password = decoded.partition(b":")  # first colon only
    if not sep:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    user_ok = hmac.compare_digest(received_user, username.encode("utf-8"))
    password_ok = hmac.compare_digest(received_password, password.encode("utf-8"))
    return user_ok and password_ok

# verify_basic_auth(request.headers.get("authorization"),
#                   os.environ.get("AWS_EVENTBRIDGE_WEBHOOK_USERNAME"),
#                   os.environ.get("AWS_EVENTBRIDGE_WEBHOOK_PASSWORD"))

```

Be clear about what Basic auth gives you. It proves the sender knows the connection's credentials. It does not cover the body, so it cannot detect a modified payload, and there is no timestamp, so a captured request can be replayed and will still pass. Use a long random password, serve the endpoint over HTTPS only (EventBridge requires it), and rotate the credentials if they might have leaked. The `User-Agent` header is easy to fake and proves nothing.

> **Make EventBridge events production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the connection's Basic credentials, queues every event durably, and delivers it to your service with retries and replay you control.

## Amazon EventBridge webhook limitations and pain points

### A static credential instead of a signature

**The Problem:** Nothing in an API destination request proves the body is unchanged or that the request is new. Anyone who learns the connection's credentials can send requests your endpoint will accept.

**Why It Happens:** Connections hold credentials for EventBridge to present, and EventBridge computes nothing over the body. API Key connections are just as static, and OAuth connections send a token your endpoint would have to validate against your own identity provider.

**Workarounds:**

* Generate a long random password and keep it out of source control and logs.
* Treat event contents as untrusted until validated, and make handlers idempotent so a replayed request does no harm.
* For high-value actions, look the resource up through the AWS API before acting on the event.

**How Hookdeck Can Help:** Hookdeck's Amazon EventBridge source checks the Basic username and password on every request. Hookdeck still answers with a `200`, but a request with missing or wrong credentials is recorded as failed verification and never reaches your destination, so your service only processes authenticated events.

### A five-second timeout

**The Problem:** Handlers that do real work inline time out, and EventBridge retries them, so slow processing turns into duplicate deliveries.

**Why It Happens:** EventBridge gives an API destination 5 seconds to respond. A timed-out request is retried under the target's retry policy, which defaults to 24 hours and 185 attempts on an event bus.

**Workarounds:**

* Authenticate, store the event, return `200`, and process afterwards.
* Deduplicate on the envelope `id`, since a request that timed out on your side may still have been processed.

**How Hookdeck Can Help:** Hookdeck responds to EventBridge as soon as the request arrives and stores it, then delivers to your service with its own timeout and [retries](/docs/retries), so a slow handler no longer burns EventBridge's retry budget.

### Events are dropped once retries run out

**The Problem:** An outage longer than the retry window loses events, and a backlog can exceed it even without an outage.

**Why It Happens:** EventBridge retries `401`, `407`, `409`, `429` and `5xx` responses until the event's maximum age or attempt count, then sends the event to a dead-letter queue if you configured one, and otherwise drops it. Other `4xx` responses are not retried at all. If the destination's invocation rate is far below the rate of matching events, AWS warns that the backlog can exceed the 24-hour window on its own.

**Workarounds:**

* Configure a dead-letter queue on every API destination target, and alert on messages arriving in it.
* Set the invocation rate to what your endpoint can sustain, and filter the event pattern down to what you need.
* Don't return a non-retried `4xx` for a transient problem in your own code.

**How Hookdeck Can Help:** Hookdeck accepts each event at the rate EventBridge sends it and delivers to your service at a [max delivery rate](/docs/destinations#set-a-max-delivery-rate) you set. [Issues](/docs/issues) alert you when deliveries fail, and you can replay any event once your service is back.

### The method and body are configuration, not a contract

**The Problem:** A receiver written for `POST` and the event envelope breaks when someone changes the destination to `PUT` or adds an input transformer.

**Why It Happens:** API destinations accept any method except `CONNECT` and `TRACE`, and an input transformer can turn the body into any JSON or text, or a `GET` can send none.

**Workarounds:**

* Accept the methods you configure on the route, and handle an empty body.
* Version input transformer templates like an API schema, and include `id`, `source` and `detail-type` in them.

**How Hookdeck Can Help:** Hookdeck's [transformations](/docs/transformations) can reshape events after they arrive, so the input transformer can stay simple, and the source's allowed methods make the accepted verbs explicit.

## Best practices

### Use one connection per receiver

Credentials live on the connection, and several API destinations can share one. A connection per receiving service lets you rotate one set of credentials without touching the others. When you rotate, update the connection and your receiver together.

### Keep the envelope unless you need a transformer

The default envelope carries `id`, `source`, `detail-type` and `time`, which is everything you need to route, deduplicate and order events. If you add an input transformer, keep those fields in its output.

### Deduplicate on `id`

Retries after timeouts and `5xx` responses mean the same event can arrive more than once. Store the envelope `id` and skip repeats. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Five seconds is a short window. Verify, persist, return `200`, and do the work afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Always configure a dead-letter queue

Without one, an event that exhausts its retries is gone. With one, you can inspect and redrive it.

## Conclusion

EventBridge API destinations deliver bus events to your HTTPS endpoint with whatever method you choose, authorized by a connection rather than a signature. With a Basic connection, compare the username and password in constant time over HTTPS, deduplicate on the envelope `id`, answer within 5 seconds, and give every target a dead-letter queue.

[Hookdeck Event Gateway](https://hookdeck.com) checks the connection's credentials, absorbs bursts, and holds each event until your service has processed it, so a slow handler or an outage on your side costs latency instead of dropped events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Amazon EventBridge events reliably in minutes.