# Enable Data Events

> [!NOTE]
> SOS Data Events for Audit-Trail is only available for customers with
> `Enterprise` support plan and are affected by a specific extra cost.

SOS Data Events for Audit-Trail give you detailed logs for all API requests
executed against the SOS service, including accepted and rejected requests,
information about the querier, and the affected objects.

Events are grouped into batches of up to 1'000 events and are submitted as
JSON-encoded payloads to a webhook you provide. Calls to the webhook will
include an HMAC-SHA256 signature stored in the `exo-audittrail-signature` HTTP
header, computed using a key given by Exoscale during the configuration process.

### Guarantees

SOS Data Events for Audit-Trail are delivered on a best-effort basis, the
exhaustivity and delivery duration of events is not guaranteed. The data event
can be used to understand the traffic against your bucket. Some events can be
lost, duplicated or significantly delayed.

Requests against the wrong region, resulting in a redirect, do not generate
any data-events and will not be visible. When the request is executed against
the right zone after following the redirect, data events will be generated.

### Configuration

The configuration is per-bucket and will include all data events. An HTTPS
webhook URL targeting port 443 and handling `POST` requests must be provided to
Exoscale support. In return you will be given a symmetric key that you can use
to validate the accompanying HMAC-SHA256 signature.

### Sample output

Here is an example JSON payload received by the webhook after executing
`show-object-cors` followed by `delete-object`:

```json
[
  {
    "api-key": null,
    "handler": "show-object-cors",
    "request-id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
    "request-method": "OPTIONS",
    "resource": "my-bucket",
    "source-ip": "1.2.3.4",
    "status": 200,
    "timestamp": "2026-01-01T01:01:01.123456789Z",
    "uri": "https://my-bucket.sos-at-vie-1.exo.io",
    "user-agent": "Mozilla/5.0 ...",
    "zone": "at-vie-1"
  },
  {
    "api-key": null,
    "handler": "delete-object",
    "iam-role": {
      "id": "dddddddd-dddd-dddd-dddd-dddddddddddd",
      "name": "Owner"
    },
    "iam-user": {
      "id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
      "role": {
        "id": "dddddddd-dddd-dddd-dddd-dddddddddddd"
      }
    },
    "request-id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
    "request-method": "DELETE",
    "resource": "my-bucket",
    "source-ip": "4.3.2.1",
    "status": 200,
    "timestamp": "2026-01-01T01:01:01.123456789Z",
    "uri": "https://my-bucket.sos-at-vie-1.exo.io/my-object-name.txt",
    "user-agent": "Mozilla/5.0 ...",
    "zone": "at-vie-1"
  }
]
```

Here is another example payload received after performing a batch delete using
`delete-objects`. Specially for `delete-objects`, the data event includes the
`body` field that contains a JSON encoding of the
[XML response](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html#API_DeleteObjects_ResponseSyntax)
sent by SOS:

```json
[
  {
    "api-key": "EXOaaaaaaaaaaaaaaaaaaaaaaaa",
    "body": {
      "DeleteResult": {
        "Deleted": [
          {
            "Key": "mykey-0acf6939"
          },
          {
            "Key": "mykey-8ba16fd3"
          }
        ]
      }
    },
    "handler": "delete-objects",
    "iam-api-key": {
      "key": "EXOaaaaaaaaaaaaaaaaaaaaaaaa",
      "name": "my-key",
      "role-id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
    },
    "iam-role": {
      "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
      "name": "Owner"
    },
    "request-id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
    "request-method": "POST",
    "resource": "my-bucket",
    "source-ip": "4.3.2.1",
    "status": 200,
    "timestamp": "2026-01-01T00:00Z",
    "uri": "https://sos-ch-dk-2.exo.io/my-bucket",
    "user-agent": "Mozilla/5.0 ...",
    "zone": "ch-dk-2"
  }
]
```

If your bucket has versioning enabled you will notice that the objects are
replaced with delete markers instead of being deleted outright:

```json
[
  {
    ...
    "body": {
      "DeleteResult": {
        "Deleted": [
          {
            "Key": "mykey-e3a00bf0",
            "DeleteMarker": true,
            "DeleteMarkerVersionId": "1245494379674602496"
          },
          {
            "Key": "mykey-f7e094ea",
            "DeleteMarker": true,
            "DeleteMarkerVersionId": "1245494379674602497"
          }
        ]
      }
    },
    "handler": "delete-objects",
    ...
  }
]
```

However, if your deletion requests target specific versions rather than bare
keys, then delete markers are not used and the versions are deleted directly:

```json
[
  {
    ...
    "body": {
      "DeleteResult": {
        "Deleted": [
          {
            "Key": "mykey-cbff453c",
            "VersionId": "1245494389376027648"
          },
          {
            "Key": "mykey-0619ee01",
            "VersionId": "1245494386049945600"
          }
        ]
      }
    },
    "handler": "delete-objects",
    ...
  }
]
```

Note that delete markers themselves can be deleted:

```json
[
  {
    ...
    "body": {
      "DeleteResult": {
      "Deleted": [
        {
          "Key": "mykey-aab0954e",
          "VersionId": "X/1245496299097165824",
          "DeleteMarker": true
        },
        {
          "Key": "mykey-dd18b9ee",
          "VersionId": "X/1245496299097165825",
          "DeleteMarker": true
        }
      ]
      }
    },
    "handler": "delete-objects",
    ...
  }
]
```

Be aware that it's possible for deletion to fail for some objects, for example
due to configured object locks mandating a retention period, etc.:

```json
[
  {
    ...
    "body": {
      "DeleteResult": {
        "Deleted": [
          {
            "Key": "mykey-cbff453c",
            "VersionId": "1245494389376027648"
          },
        ],
        "Errors": [
          {
            "Key": "mykey-bbcf5de5",
            "VersionId": "1245494399173922816",
            "Message": "Access denied"
          }
      }
    },
    "handler": "delete-objects",
    ...
  }
]
```

### Webhook example

As a starting point, here is an example of a webhook which can be used to
receive, validate and process data events.

```python3
import json
import base64
import hashlib
import hmac
from http.server import BaseHTTPRequestHandler

....

class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
    signing_key = None

    ....

    def do_POST(self):
        content_length = int(self.headers.Get("Content-Length", 0))
        body = self.rfile.read(content_length)

        # Verify HMAC-SHA256 signature
        signature_header = self.headers.get("exo-audittrail-signature")
        if not signature_header:
            self.send_response(400)
            self.end_headers()
            self.wfile.write(b"Bad Request: Missing signature header")
            return

        try:
            # Decode the signing key from base64
            key = base64.b64decode(self.signing_key)
            # Compute expected signature
            expected_signature = hmac.new(key, body, hashlib.sha256).hexdigest()

            if not hmac.compare_digest(signature_header, expected_signature):
                self.send_response(400)
                self.end_headers()
                self.wfile.write(b"Bad Request: Invalid signature")
                return
        except Exception as e:
            self.send_response(400)
            self.end_headers()
            self.wfile.write(b"Bad Request: Signature verification error")
            return

        data = json.loads(body.decode("utf-8"))

        ....

        self.send_response(200)
        self.end_headers()
```

