How to Receive Webhooks from MinIO Bucket Notifications (Self-Hosted, Step by Step)

Receive MinIO S3 bucket event notifications on your own server with Bearer token authentication, mc event setup, queue_dir reliability, and RabbitMQ routing, verified end to end against a real MinIO.

By the end of this guide you will have a self-hosted endpoint that receives MinIO bucket notifications, authenticates every delivery with a Bearer token, and routes the events to a log, RabbitMQ, or any other destination. Everything below was verified end to end against a real MinIO server and the real Core Webhook Module Docker image: real object uploads triggered the deliveries quoted here, and the Authorization header format was confirmed by capturing MinIO's actual requests.

How MinIO webhook notifications work

MinIO publishes S3-style bucket events (object created, deleted, and more) to configured notification targets, one of which is a plain HTTP webhook. When an event fires, MinIO POSTs a JSON document:

{
  "EventName": "s3:ObjectCreated:Put",
  "Key": "demo-bucket/hello.txt",
  "Records": [
    {
      "eventVersion": "2.0",
      "eventSource": "minio:s3",
      "eventTime": "2026-08-29T18:59:39.623Z",
      "eventName": "s3:ObjectCreated:Put",
      "userIdentity": {"principalId": "minioadmin"},
      "s3": {
        "s3SchemaVersion": "1.0",
        "bucket": {"name": "demo-bucket", "arn": "arn:aws:s3:::demo-bucket"},
        "object": {"key": "hello.txt", "size": 14, "eTag": "...", "contentType": "text/plain"}
      }
    }
  ]
}

The Records array follows the AWS S3 event schema, so tooling built for S3 notifications parses it unchanged.

Authentication: MinIO does not sign webhook payloads. Instead, each webhook target takes an auth_token, and MinIO sends it on every request as Authorization: Bearer <auth_token> (verified by capturing a live delivery; the request arrives with User-Agent: Go-http-client/1.1 and Content-Type: application/json). The gateway compares the token in constant time.

Reliability: each webhook target supports an optional queue_dir, a directory where MinIO persists undelivered events while your endpoint is unreachable, and queue_limit caps that store. Set queue_dir in production; without it, events fired while the receiver is down are gone.

Prerequisites

Step 1: Configure the receiver

Create a working directory with two files.

webhooks.json:

{
    "minio_events": {
        "data_type": "json",
        "module": "log",
        "authorization": "Bearer {$MINIO_WEBHOOK_TOKEN}"
    }
}

connections.json:

{}

What each parameter does:

  • minio_events is the webhook ID; it becomes the URL path (/webhook/minio_events).
  • module: log prints each authenticated event to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.
  • authorization requires the exact Authorization: Bearer <token> header MinIO sends and compares it in constant time.
  • {$MINIO_WEBHOOK_TOKEN} pulls the token from an environment variable, so it never lives in a config file.

Generate a strong token (or use our webhook secret generator):

openssl rand -hex 32

Step 2: Run it

docker run -d --name webhook-gateway -p 8000:8000 \
  -v "$PWD/webhooks.json:/app/webhooks.json:ro" \
  -v "$PWD/connections.json:/app/connections.json:ro" \
  -e MINIO_WEBHOOK_TOKEN="your-generated-token" \
  spiderhash/webhook:latest

Confirm it is up:

docker logs webhook-gateway
# ... INFO: Application startup complete.

The receiver now answers on http://localhost:8000/webhook/minio_events, with auto-generated API docs at http://localhost:8000/docs.

Step 3: Configure the webhook target in MinIO

Two equivalent ways; both need a server restart to take effect.

With environment variables (the identifier PRIMARY is your choice):

MINIO_NOTIFY_WEBHOOK_ENABLE_PRIMARY=on
MINIO_NOTIFY_WEBHOOK_ENDPOINT_PRIMARY=https://your-domain/webhook/minio_events
MINIO_NOTIFY_WEBHOOK_AUTH_TOKEN_PRIMARY=your-generated-token
MINIO_NOTIFY_WEBHOOK_QUEUE_DIR_PRIMARY=/events

Or with mc admin config set:

mc admin config set myminio notify_webhook:PRIMARY \
  endpoint="https://your-domain/webhook/minio_events" \
  auth_token="your-generated-token" \
  queue_dir="/events"
mc admin service restart myminio

On startup MinIO logs the target's ARN: SQS ARNs: arn:minio:sqs::PRIMARY:webhook. Now subscribe a bucket to it:

mc event add myminio/demo-bucket arn:minio:sqs::PRIMARY:webhook --event put
mc event ls myminio/demo-bucket arn:minio:sqs::PRIMARY:webhook
# arn:minio:sqs::PRIMARY:webhook   s3:ObjectCreated:*   Filter:

--event accepts put, get, delete, and more; --prefix and --suffix filter by object key.

Step 4: Test it

Upload an object; that is the native test mechanism:

echo 'hello webhook' > hello.txt
mc cp hello.txt myminio/demo-bucket/hello.txt

The event lands immediately; docker logs webhook-gateway shows the s3:ObjectCreated:Put payload and the 200 OK response.

You can also simulate the delivery directly against the gateway. Save the example payload from the top of this article as event.json, then:

curl -s -w "\nHTTP %{http_code}\n" -X POST http://localhost:8000/webhook/minio_events \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-generated-token" \
  --data-binary @event.json

Expected result:

{"message":"200 OK"}
HTTP 200

A wrong token:

{"detail":"Unauthorized"}
HTTP 401

And a request with no Authorization header at all:

{"detail":"Invalid Bearer token format: must start with 'Bearer '"}
HTTP 401

Those are the exact responses the gateway returns.

Troubleshooting

  • 401 {"detail":"Unauthorized"}: the auth_token on the MinIO target and MINIO_WEBHOOK_TOKEN differ. They must be byte-identical.
  • Config set but nothing sends: both the env var and mc admin config set routes require a server restart (mc admin service restart). Also check the bucket subscription exists with mc event ls; the target ARN and the subscription are separate steps.
  • Events lost while the receiver was down: configure queue_dir (a writable directory inside the MinIO container) so MinIO persists undelivered events and replays them when the endpoint is back.
  • The payload looks like an S3 notification: it is. Match on EventName or Records[].eventName (s3:ObjectCreated:Put, s3:ObjectRemoved:Delete, ...) and read the object under Records[].s3.object.

Going to production

Printing to logs is not a pipeline. Swap the module for a real destination; here is the RabbitMQ variant, verified end to end (a real mc cp upload landed as a message in the minio_events queue):

webhooks.json:

{
    "minio_events": {
        "data_type": "json",
        "module": "rabbitmq",
        "connection": "rabbitmq_local",
        "module-config": {
            "queue_name": "minio_events"
        },
        "authorization": "Bearer {$MINIO_WEBHOOK_TOKEN}"
    }
}

connections.json:

{
    "rabbitmq_local": {
        "type": "rabbitmq",
        "host": "{$RABBITMQ_HOST:localhost}",
        "port": "{$RABBITMQ_PORT:5672}",
        "user": "{$RABBITMQ_USER:guest}",
        "pass": "{$RABBITMQ_PASS:guest}"
    }
}

The same pattern works for 17 other destinations: mirror event metadata to S3, publish to Kafka, or fan out to several at once with webhook chaining. Built-in rate limiting and retry handling cover the operational edges; a bulk upload that fires thousands of events is exactly when you want the gateway acknowledging fast and queueing durably. The full authentication reference, including Bearer tokens and 11 other methods, is in the docs.

Subscribe to Free Webhook Tool

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe