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.
In the dashboard
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.
Before you begin
Section titled Before you begin- Sign in to the dashboard and open the service. The service must be
activeto add, change or test a rule. - Have an address that accepts a
POST, such ashttps://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
2xxstatus within 10 seconds. Do the slow work after you answer.
Add a rule
Section titled Add a rule- In the dashboard, go to Object Storage, open the service, then the Buckets tab. Open the bucket, then Notifications.
- Select Add rule….
- In Address, enter the address to send events to, starting with
https://. - Under Events, tick Object created, Object deleted or both. Events lists what each covers.
- To send events for some keys only, fill in Prefix, Suffix or both. For example, Prefix
photos/and Suffix.jpgcoversphotos/2026/beach.jpgand leaves outphotos/notes.txt. Both are matched exactly, capitals included. Leave both empty to cover every key. - Optionally, enter a Description, such as
Thumbnail builder, to tell rules apart in the list. - Select Add rule.
The dialog warns you before you save when the rule overlaps another one (Overlapping rules).
Result
Section titled ResultA 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
Ruleand 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;Failingwhen the latest delivery failed and none has worked since;Pausedwhen 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.
Events
Section titled Events| 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:Putagain, even with the same content. - In a bucket with versioning on, each new version is an
ObjectCreated:Putwith itsversionId. Deleting one version for good isObjectRemoved:Deletewith that version'sversionId. - 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.
Overlapping rules
Section titled Overlapping rulesTwo 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.
What we send
Section titled What we sendEach delivery is one POST to the rule's address, with one event in it:
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
{
"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.jpghere). sizeandeTag- Sent with created events. A multipart upload's
eTagends 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:
{"Bucket":"u7-assets","ConfigurationId":"31","Event":"s3:TestEvent","Service":"Coritan Object Storage","Time":"2026-10-10T11:52:03.456Z"}
Check the signature
Section titled Check the signatureX-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:
- Read the raw body before any JSON parser changes it.
- Compute the HMAC of
t, a full stop, and the body, with the signing secret as the key. - Compare it with
v1in constant time, and refuse the message when they differ. - Refuse a
tmore than five minutes from your clock, so an old message cannot be sent to you again.
In Python:
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):
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:
{"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.
Retries and pausing
Section titled Retries and pausingA 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.
Read the delivery log
Section titled Read the delivery logThe 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;QueuedorWaiting to retry, with when the next attempt is due;Sending;Failedafter the last attempt;Not sentwhen the rule was paused or deleted first.- Response
- The status your address answered and the start of its reply, or
No answerwhen 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 ruleOpen 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.
Rotate the signing secret
Section titled Rotate the signing secretSelect 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.
Address rules
Section titled Address rulesWe 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.
Limits
Section titled Limits| 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 |
With the API
Section titled With the APIEvery 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:
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.
Troubleshooting
Section titled TroubleshootingThe 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.5or127.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
401or403usually means your receiver refused the signature: check it uses the latest secret and the raw body.No answermeans 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.
Related
Section titled RelatedAPI operations on this page
| Method | Path | What it does |
|---|---|---|
GET | /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/notifications | The bucket's event notification rules, oldest first |
POST | /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/notifications | Add a rule: POST each matching event in the bucket to url |
PUT | /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/notifications/{rule_id} | Change a rule |
DELETE | /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/notifications/{rule_id} | Delete a rule |
POST | /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/notifications/{rule_id}/test | Send test notification |
POST | /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/notifications/{rule_id}/rotate-secret | Replace the rule's signing secret and answer the new one, shown this once |
GET | /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/notifications/{rule_id}/deliveries | The rule's deliveries of the last 30 days, newest first |