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
- Docker on any machine (your laptop is fine for the test run)
- A public HTTPS URL for production, unless MinIO and your receiver share a network. Two ways to get one:
- HTTPS for webhooks with nginx and Let's Encrypt if you have a public server
- Receiving webhooks through Cloudflare Tunnel if you do not want to open inbound ports
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_eventsis the webhook ID; it becomes the URL path (/webhook/minio_events).module: logprints each authenticated event to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.authorizationrequires the exactAuthorization: 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"}: theauth_tokenon the MinIO target andMINIO_WEBHOOK_TOKENdiffer. They must be byte-identical.- Config set but nothing sends: both the env var and
mc admin config setroutes require a server restart (mc admin service restart). Also check the bucket subscription exists withmc 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
EventNameorRecords[].eventName(s3:ObjectCreated:Put,s3:ObjectRemoved:Delete, ...) and read the object underRecords[].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.