---
title: "React to uploads with bucket events (webhooks)"
description: "Get a signed HTTPS call when an object is uploaded or deleted: thumbnails, virus scans, indexing, sync."
url: https://www.gyrohosting.com/developers/en/howto/s3-events.html
language: en
openapi: https://www.gyrohosting.com/developers/openapi.json
api_version: 2.37.0
---

# React to uploads with bucket events (webhooks)

Get a signed HTTPS call when an object is uploaded or deleted: thumbnails, virus scans, indexing, sync.

A **bucket event** sends a POST to your HTTPS address when an object is created (upload, copy, multipart upload), deleted, or a bulk delete runs. It works on Object Storage and Protected Storage. The body never contains the file, only its key, size, ETag and time.


## 1. Add a webhook

**curl**

```bash
curl -s -X POST https://www.gyrohosting.com/api/v1/client/cloud/storage/12345/buckets/gyro-proje/events \
  -H "Authorization: Bearer $GYRO_TOKEN" -H "Content-Type: application/json" \
  -d '{"url":"https://ornek.com/s3-olay","events":["storage.object.created"],"prefix":"resimler/","suffix":[".jpg",".png"]}'
# the answer contains "secret" ONCE: store it
```

**CLI**

```bash
gyro storage events add 12345 gyro-proje --url https://ornek.com/s3-olay --events storage.object.created --prefix resimler/ --suffix .jpg,.png
gyro storage events test 12345 7
gyro storage events deliveries 12345 7
gyro storage events usage 12345
```

**Terraform**

```hcl
resource "gyro_storage_bucket_notification" "thumbnails" {
  service_id = 12345
  bucket     = "gyro-proje"
  url        = "https://ornek.com/s3-olay"
  events     = ["storage.object.created"]
  prefix     = "resimler/"
  suffix     = [".jpg", ".png"]
}
```


## 2. Verify the signature

Every request has `X-Gyro-Event`, `X-Gyro-Delivery` (unique id), `X-Gyro-Timestamp` and `X-Gyro-Signature: sha256=HMAC-SHA256(secret, timestamp + "." + raw body)` - the same format as account webhooks.

**Node.js**

```
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/s3-olay", express.raw({ type: "application/json" }), (req, res) => {
  const ts = req.get("X-Gyro-Timestamp");
  const want = "sha256=" + crypto.createHmac("sha256", process.env.GYRO_EVENT_SECRET).update(ts + "." + req.body).digest("hex");
  const got = req.get("X-Gyro-Signature") || "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300 || got.length !== want.length ||
      !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))) return res.status(401).end();
  const ev = JSON.parse(req.body);          // drop duplicates with req.get("X-Gyro-Delivery")
  console.log(ev.event, ev.data.bucket, ev.data.key, ev.data.size);
  res.status(204).end();                    // answer within 6 s; do slow work in the background
});
app.listen(8080);
```

**Python**

```python
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["GYRO_EVENT_SECRET"].encode()
@app.post("/s3-olay")
def s3_olay():
    ts, body = request.headers.get("X-Gyro-Timestamp", ""), request.get_data()
    want = "sha256=" + hmac.new(SECRET, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    if abs(time.time() - int(ts or 0)) > 300 or not hmac.compare_digest(want, request.headers.get("X-Gyro-Signature", "")):
        abort(401)
    ev = request.get_json()   # X-Gyro-Delivery: unique id, ignore repeats
    print(ev["event"], ev["data"]["bucket"], ev["data"].get("key"))
    return "", 204
```


## 3. Body

**JSON**

```
{"id":"0b3c7c9e-6a0e-4c43-9b7b-2f5f1c1c9a11","event":"storage.object.created","created_at":"2026-11-15T10:12:04+03:00",
 "data":{"service_id":12345,"bucket":"gyro-proje","event_time":"2026-11-15T10:12:04+03:00","method":"put","source":"api",
         "key":"resimler/kapak.jpg","access_key_id":"GKxxxxxxxxxxxxxxxxxxxxxxxx","size":248133,"etag":"9b2cf535f27731c974343645a3985328","content_type":"image/jpeg"}}
```

Events: `storage.object.created` (method put / multipart), `storage.object.deleted`, `storage.objects.deleted_bulk` (keys not listed), `storage.test`. Protected Storage adds `version_id`.


## Delivery, limits, price

At least once, no ordering guarantee: use `X-Gyro-Delivery` to drop repeats. Answer with 2xx within 6 seconds; anything else is retried 8 times over about 2 hours; 20 failures in a row disable the webhook (re-enable with `PATCH status=active`). Requests come from 45.87.174.151, 45.87.174.153 and 45.87.174.154. Targets must be https on port 443 or 8443 on the public internet. 5 webhooks per bucket, 20 per service. A monthly free allowance per bucket plus prepaid event packs (valid in the month bought); no overage invoice: when the allowance is used up new events are simply not sent (`GET .../events/usage`). Objects removed by a lifecycle rule produce no event.
