Skip to content
Coritan Docs

Get notified when objects change

Send a signed HTTPS POST to your own address each time an object in a bucket is created or deleted, and check each message's signature.

View as Markdown

An event notification rule tells us to send a message to your own HTTPS address each time an object in a bucket is created or deleted. Use one to build thumbnails when a photo arrives, to update a search index when a file goes, or to keep your own records in step with a bucket. Each message is an S3 event record in JSON, signed with a secret only you and we know.

You manage rules in the bucket's Notifications section or with the API. S3's own PutBucketNotificationConfiguration call is not supported: S3 clients cannot change a bucket's rules.

  • Sign in to the dashboard and open the service. The service must be active to add, change or test a rule.
  • Have an address that accepts a POST, such as https://example.com/hooks/storage. It must use HTTPS, be on the public internet and be outside Coritan (Address rules).
  • Make sure the address answers with a 2xx status within 10 seconds. Do the slow work after you answer.
  1. In the dashboard, go to Object Storage, open the service, then the Buckets tab. Open the bucket, then Notifications.
  2. Select Add rule….
  3. In Address, enter the address to send events to, starting with https://.
  4. Under Events, tick Object created, Object deleted or both. Events lists what each covers.
  5. To send events for some keys only, fill in Prefix, Suffix or both. For example, Prefix photos/ and Suffix .jpg covers photos/2026/beach.jpg and leaves out photos/notes.txt. Both are matched exactly, capitals included. Leave both empty to cover every key.
  6. Optionally, enter a Description, such as Thumbnail builder, to tell rules apart in the list.
  7. Select Add rule.

The dialog warns you before you save when the rule overlaps another one (Overlapping rules).

A dialog shows the rule's signing secret. Copy it and store it where your receiver can read it, then select I have saved it. We show the secret only this once. If you lose it, rotate it to get a new one (Rotate the signing secret).

The rule appears on the Event notifications card, and it covers changes made from that moment on. Changes made before you added it are not sent. The card lists each rule with:

Rule
The description, or Rule and its number, with the address under it.
Events
Object created, Object deleted or both.
Keys
All keys, or the prefix and suffix a key must have.
Status
Active; Failing when the latest delivery failed and none has worked since; Paused when you or we paused the rule.
Last delivery
When the latest delivery worked or failed, and how many events are waiting.

To check the rule end to end, open its menu and select Send test event. A message tells you what your address answered, and the test appears in the Delivery log.

Event What sends it eventName
Object created An upload (PutObject), a copy (CopyObject), a browser form upload (PostObject) ObjectCreated:Put
Object created A finished multipart upload (CompleteMultipartUpload) ObjectCreated:CompleteMultipartUpload
Object deleted A delete (DeleteObject), each key of a batch delete (DeleteObjects) ObjectRemoved:Delete
Object deleted A delete in a bucket with versioning on that leaves a delete marker ObjectRemoved:DeleteMarkerCreated
Object deleted A lifecycle rule of the bucket removing an object LifecycleExpiration:Delete

A few details:

  • A copy and a browser form upload arrive as ObjectCreated:Put, as an upload does.
  • Writing an object again sends ObjectCreated:Put again, even with the same content.
  • In a bucket with versioning on, each new version is an ObjectCreated:Put with its versionId. Deleting one version for good is ObjectRemoved:Delete with that version's versionId.
  • A change that neither creates nor deletes an object, such as adding tags to it, sends nothing.
  • Changes made from the dashboard and with any access key are all sent.

We look for changes once a minute and send new events once a minute, so an event reaches your address a minute or two after the change.

Two rules on one bucket overlap when they share an event and one key could match both. That is the case when one rule's prefix starts with the other's and one rule's suffix ends with the other's. An empty prefix overlaps every prefix, and an empty suffix every suffix.

Rule A Rule B Overlap
Prefix photos/ Prefix photos/2026/ Yes: photos/2026/a.jpg matches both.
Prefix photos/, Suffix .jpg Prefix photos/, Suffix .png No: a key cannot end in both.
Prefix photos/ Prefix videos/ No.
Object created on every key Object deleted on every key No: they share no event.

We refuse a rule that overlaps another with This rule overlaps rule <id>, and the dialog shows the same warning before you save. To send one change to two addresses, have one receiver pass it on. To change what a rule covers, edit it instead of adding a second one.

Each delivery is one POST to the rule's address, with one event in it:

HTTP
POST /hooks/storage HTTP/1.1
Content-Type: application/json
User-Agent: Coritan-Object-Storage-Events/1.0
X-Coritan-Event: ObjectCreated:Put
X-Coritan-Delivery: 9100
X-Coritan-Signature: t=1791633125,v1=ede87cf763a0dab52279c729ce7348346f503f1d1e88eafefc165a2bff5258e8
JSON
{
  "Records": [
    {
      "awsRegion": "fra",
      "eventName": "ObjectCreated:Put",
      "eventSource": "coritan:s3",
      "eventTime": "2026-10-10T11:52:03.456Z",
      "eventVersion": "2.1",
      "s3": {
        "bucket": { "name": "u7-assets" },
        "configurationId": "31",
        "object": {
          "eTag": "9b2cf535f27731c974343645a3985328",
          "key": "photos/red+flower.jpg",
          "sequencer": "18DD28D5536B2000",
          "size": 48213
        },
        "s3SchemaVersion": "1.0"
      }
    }
  ]
}

The record has the shape of an S3 event record, so code written for S3 events reads it:

key
The object's key, URL-encoded as S3 encodes it: a space is +. Decode it before you use it (photos/red flower.jpg here).
size and eTag
Sent with created events. A multipart upload's eTag ends in - and the number of parts.
versionId
Sent when the bucket has versioning on.
sequencer
Compare two events for the same key as strings: the larger one is the later change. Deliveries can arrive out of order.
configurationId
The rule's ID.
awsRegion
The code of the bucket's region.

X-Coritan-Delivery is the delivery's number, the same on every attempt. If your address took a message but answered too late, we send it again, so use the number to skip one you have handled.

Send test event sends this body instead:

JSON
{"Bucket":"u7-assets","ConfigurationId":"31","Event":"s3:TestEvent","Service":"Coritan Object Storage","Time":"2026-10-10T11:52:03.456Z"}

X-Coritan-Signature holds t=<unix time>,v1=<signature>. The signature is the hex HMAC-SHA256 of <t>.<body>, keyed with the rule's signing secret, where <body> is the request body exactly as it arrived. We sign each attempt when we send it.

To check a message:

  1. Read the raw body before any JSON parser changes it.
  2. Compute the HMAC of t, a full stop, and the body, with the signing secret as the key.
  3. Compare it with v1 in constant time, and refuse the message when they differ.
  4. Refuse a t more than five minutes from your clock, so an old message cannot be sent to you again.

In Python:

verify.py
import hashlib
import hmac
import time


def verify(body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode(), parts.get("v1", "").encode())

In Node.js, with the body as a Buffer (in Express, express.raw({ type: 'application/json' }) gives you one):

verify.mjs
import crypto from 'node:crypto';

export function verify(body, header, secret, tolerance = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => [p.slice(0, p.indexOf('=')), p.slice(p.indexOf('=') + 1)]));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > tolerance) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(body).digest();
  const given = Buffer.from(parts.v1 || '', 'hex');
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

To test your code, this body:

Text
{"Bucket":"u7-assets","ConfigurationId":"31","Event":"s3:TestEvent","Service":"Coritan Object Storage","Time":"2026-10-10T11:52:03.456Z"}

signed at t=1791633125 with the secret example-secret gives v1=e398419dd7237b7e3484c6a65fcd562ef5dc056ab280f1fcd08e6971e2c3c174. Pass a large tolerance when you check it, because that moment has passed.

A delivery works when your address answers with a 2xx status within 10 seconds. Any other status, a redirect, a timeout or a failed connection is a failed attempt. We do not follow redirects.

After a failed attempt we try again 1 minute later, then after 5 minutes, 30 minutes, 2 hours and 6 hours. A delivery that fails all six attempts is Failed, and we do not send it again.

When every delivery of a rule has failed for a day, with none working since the first failure, we pause the rule:

  • We stop sending to its address and drop the events waiting to be sent.
  • We email the account's owner.
  • A warning above the rules names the paused rule and offers Turn on.

Fix the address, select Send test event to check it (a test works on a paused rule), then select Turn on. The rule sends changes made from then on. Changes made while it was paused are not sent.

The Delivery log card under the rules shows what we sent for one rule in the last 30 days, newest first, 25 to a page. Select Show deliveries in a rule's menu to show that rule's log, and use the lists above the log to choose another rule or one status. The address keeps both choices, such as ?rule=31&status=failed, so you can bookmark or share the view.

Event
The event name and the object's key. A test shows Test event.
Status
Delivered; Queued or Waiting to retry, with when the next attempt is due; Sending; Failed after the last attempt; Not sent when the rule was paused or deleted first.
Response
The status your address answered and the start of its reply, or No answer when it did not answer.
When
When the change happened, and the object's size.

Change, pause or delete a rule

Section titled Change, pause or delete a rule

Open the rule's menu on the Event notifications card:

  • Edit rule… changes the address, the events, the filters or the description. The same checks apply as when you add a rule.
  • Pause rule… stops sending and drops the events waiting. Turn on starts it again for changes made from then on.
  • Delete rule… asks you to type the rule's name. Events waiting to be sent are dropped, and its delivery log goes with it.

Caution

Deleting a rule cannot be undone. The events it would have sent while it was gone are not sent if you add it again.

Select Rotate signing secret… in the rule's menu. The dialog shows the new secret once. The old one stops at once: every message from then on is signed with the new secret, including retries of earlier events. Update your receiver straight away. Messages it refuses in the meantime fail and are tried again on the schedule above.

We refuse an address that:

  • does not start with https://;
  • holds a user name or password (check the signature instead);
  • is a name or address of Coritan's own;
  • resolves to a private, loopback or link-local network;
  • is longer than 2048 characters.

We check the address again before every delivery, so a name that later resolves to a private network fails then.

What Limit
Rules per bucket 100
Prefix and Suffix 1024 characters each
Description 100 characters
Answer we wait for 10 seconds
Attempts per delivery 6
Delivery log 30 days
Rule changes (add, edit, delete, rotate) 120 an hour per account
Test events 30 an hour per account

Every call needs an access token ($CORITAN_TOKEN). The service ID and the bucket ID come from GET /api/v1/client/object-storage/{service_id}/buckets.

Add a rule. The answer holds the rule and its signing_secret, shown this once:

Shell
curl -X POST https://api.coritan.com/api/v1/client/object-storage/106/buckets/501/notifications \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hooks/storage", "events": ["object-create"], "prefix": "photos/", "suffix": ".jpg", "description": "Thumbnail builder"}'

events takes object-create, object-delete or both. The other operations:

Operation What it does
GET .../notifications Lists the rules, with limits and what each event covers. The secret is never listed.
PUT .../notifications/{rule_id} Changes the fields you send. "enabled": false pauses the rule; true turns it back on.
DELETE .../notifications/{rule_id} Deletes the rule.
POST .../notifications/{rule_id}/test Sends a test event now and answers delivered, response_status and response_body.
POST .../notifications/{rule_id}/rotate-secret Answers a new signing_secret.
GET .../notifications/{rule_id}/deliveries The delivery log, newest first. Takes status, limit (1–200, 50 by default) and before_id, the next_before_id of the previous page.

A refused change answers 422 for a bad address, event or filter, 409 for an overlap, a bucket with 100 rules or a service that is not active, and 429 past the hourly limits. The detail says which.

The address must start with https://
Use an HTTPS address. We send nothing over plain HTTP.
The address must be outside Coritan
The address is one of Coritan's own names. Use your own domain.
The address must be on the public internet, not a private, loopback or link-local network
The name resolves to an address such as 10.0.0.5 or 127.0.0.1. Point it at a public address.
We could not find that host name
The name does not resolve. Check its spelling and its DNS records.
This rule overlaps rule <id>
Another rule on the bucket covers the same event for some of the same keys. Change the Prefix or Suffix so no key matches both, or edit the other rule (Overlapping rules).
The rule shows Failing
Open the Delivery log and read Response. A 401 or 403 usually means your receiver refused the signature: check it uses the latest secret and the raw body. No answer means we could not connect or your address took longer than 10 seconds.
The signature never matches
Your framework parsed and rewrote the body before you hashed it. Hash the bytes as they arrived, and key the HMAC with the secret exactly as shown, with no spaces or line break.
No event arrives for a change
Check that the rule is Active, that its Events, Prefix and Suffix cover the key, and that the change happened after you added or turned on the rule. Allow a minute or two.

API operations on this page