OpenAPI Türkçe gyrohosting.com ↗

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 -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
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
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.

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);
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#

{"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.