# DeleteObjects

DeleteObjects removes up to 1,000 objects from one bucket in a single request, which saves a
round trip per object when there is a lot to tidy away. Each entry names a key and, if you like,
a version ID. The response reports how every entry went - or, in quiet mode, only the ones that
did not go through. For a single object, [DeleteObject](https://docs.strg.com/s3-api/DeleteObject) does the same job.

Every STRG bucket keeps every version of its objects, so what an entry does depends on
whether it carries a version ID:

| Entry | Effect |
| --- | --- |
| Key only | Writes a delete marker. The object disappears from listings and reads, but every earlier version is kept - and billed as stored data - until it is deleted by its version ID or a [lifecycle policy](https://docs.strg.com/lifecycle-policies) removes it |
| Key and `VersionId` | Permanently deletes that version. Naming a delete marker's version ID removes the marker, so the previous version becomes current again |

**Info: Good to know on STRG**

- For an access key limited to an object prefix, every key in the request is checked against the
  prefix up front, so a request either stays inside the prefix as a whole or deletes nothing.
- A key with a `.` or `..` path segment (`a/../b`) cannot be written into a URL, and
  DeleteObjects is the way to delete one: its keys travel in the request body.
- Per-object conditions (`ETag`, `LastModifiedTime`, `Size`) and `x-amz-mfa` are answered with
  `501 NotImplemented`, so a client knows straight away.
- `x-amz-expected-bucket-owner` is welcome and has nothing to check: a key only ever reaches
  buckets of its own account.

## When you'd use it

- **Clearing out a batch of files.** A photo app whose user deletes an album can remove all of
  its pictures in one request rather than one request per picture.
- **Cleaning up after tests.** A CI pipeline that writes objects under a run's own prefix can
  list them at the end and delete them in batches of 1,000, so test data does not pile up as
  stored data.
- **Freeing up stored data for good.** Because every bucket keeps old versions, a clean-up
  script can list them with [ListObjectVersions](https://docs.strg.com/s3-api/ListObjectVersions) and delete them by
  version ID, which is what stops them being billed.
- **An agent tidying up after itself.** An AI agent can delete the files it no longer needs -
  with a human confirming first whenever an entry names a version, unless its key allows it to
  act without confirmation.

## Request

```http
POST /?delete HTTP/1.1
Host: my-bucket.s3.eu-central-1.strg.com
Authorization: AWS4-HMAC-SHA256 Credential=...
x-amz-date: 20260926T120000Z
x-amz-content-sha256: <SHA-256 of the body>
Content-MD5: <base64 MD5 of the body>
Content-Type: application/xml

<Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Object>
    <Key>photos/2024/cat.jpg</Key>
  </Object>
  <Object>
    <Key>photos/2024/dog.jpg</Key>
    <VersionId>VersionId</VersionId>
  </Object>
  <Quiet>false</Quiet>
</Delete>
```

The path-style form is `POST /my-bucket?delete` on `s3.eu-central-1.strg.com` -
[Addressing](https://docs.strg.com/s3-api/overview#addressing) explains both forms.

### Permissions

What the access key needs depends on the entries in the request -
[Access keys](https://docs.strg.com/access-keys) describes each permission.

| Entry | Permission |
| --- | --- |
| Key only (writes a delete marker) | **Write objects** (`objects:write`) |
| Key and `VersionId` (permanent delete) | **Delete object versions** (`objects:delete-versions`) |
| With `x-amz-bypass-governance-retention: true` | Also **Bypass governance retention** (`object-retention:bypass-governance`) |

A key limited to certain buckets reaches only those buckets. A key limited to an
[object prefix](https://docs.strg.com/access-keys#object-prefix) is checked against **every** key in the
request. STRG checks all the entries before deleting anything, so a single entry the access
key may not delete - outside the prefix, or with a version ID when the key does not hold
**Delete object versions** - answers the whole request with `403 AccessDenied`, and nothing is
deleted. A request is therefore all within the key's reach or left untouched.

### URI parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Bucket` | String | Yes | The bucket holding the objects. |
| `delete` | Flag | Yes | Selects this operation. Sent without a value. |

### Request headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Content-MD5` | String | Yes, unless an `x-amz-checksum-*` header is sent | The base64-encoded MD5 digest of the request body. A request with neither is answered with `400 InvalidRequest`. The AWS SDKs and the AWS CLI send an integrity header automatically, so this is taken care of. |
| `x-amz-checksum-crc32`, `x-amz-checksum-crc32c`, `x-amz-checksum-crc64nvme`, `x-amz-checksum-sha1`, `x-amz-checksum-sha256` | String | No | A checksum of the request body, accepted in place of `Content-MD5`. |
| `x-amz-sdk-checksum-algorithm` | String | No | The algorithm the SDK used for the checksum header. |
| `x-amz-bypass-governance-retention` | Boolean | No | `true` deletes object versions locked in governance mode. Needs **Bypass governance retention**. It has no effect on compliance mode - [Bypassing governance retention](https://docs.strg.com/object-locks#bypassing-governance-retention) has the details. |
| `x-amz-mfa` | String | No | STRG has no MFA delete, and answers `501 NotImplemented`. |
| `x-amz-request-payer` | String | No | Has nothing to do: STRG has no Requester Pays buckets. |
| `x-amz-expected-bucket-owner` | String | No | Welcome, and has nothing to check: an access key only ever reaches its own account's buckets. |

### Request body

A `Delete` document of up to 2 MiB (2,097,152 bytes) - room for 1,000 keys of the longest
length. A larger body is answered with `400 MaxMessageLengthExceeded`.

| Element | Type | Required | Description |
| --- | --- | --- | --- |
| `Delete` | Container | Yes | The root element. |
| `Delete/Object` | Container | Yes | One entry per object or version to delete. At least 1, at most 1,000. |
| `Delete/Object/Key` | String | Yes | The object key. Not empty. |
| `Delete/Object/VersionId` | String | No | The version to delete permanently. Without it, the entry writes a delete marker instead. |
| `Delete/Object/ETag` | String | No | A conditional delete, which STRG answers with `501 NotImplemented`. |
| `Delete/Object/LastModifiedTime` | Timestamp | No | A conditional delete, which STRG answers with `501 NotImplemented`. |
| `Delete/Object/Size` | Long | No | A conditional delete, which STRG answers with `501 NotImplemented`. |
| `Delete/Quiet` | Boolean | No | `true` returns only the entries that failed. Defaults to `false`, which returns every entry. At most once. |

STRG reads the document strictly, so every key is read exactly the way it was meant.
Comments, `CDATA` sections, a `DOCTYPE`, processing instructions (other than a leading
`<?xml …?>` declaration), namespace prefixes, attributes other than `xmlns`, unknown elements
and entities other than XML's five predefined ones and character references are answered with
`400 MalformedXML`. The documents the AWS SDKs and the AWS CLI build are all welcome as they
are.

## Response

A successful request answers with a `DeleteResult` document. The request succeeds as a whole even
when some entries do not, so the `Error` elements in the body are where an entry that failed
shows up.

### Response headers

| Name | Description |
| --- | --- |
| `x-amz-request-id` | The request ID, for support requests. |

`x-amz-request-charged` is never sent, since STRG has no Requester Pays buckets.

### Response body

| Element | Description |
| --- | --- |
| `DeleteResult` | The root element. |
| `DeleteResult/Deleted` | One per entry that succeeded. Omitted in quiet mode. |
| `Deleted/Key` | The object key. |
| `Deleted/VersionId` | The version that was deleted, for an entry that named one. |
| `Deleted/DeleteMarker` | `true` when a delete marker was written, or when the version deleted was itself a delete marker. |
| `Deleted/DeleteMarkerVersionId` | The version ID of the delete marker written, for an entry without a version ID. |
| `DeleteResult/Error` | One per entry that failed. |
| `Error/Key` | The object key. |
| `Error/VersionId` | The version ID named in the entry, if any. |
| `Error/Code` | An S3 error code, such as `AccessDenied`. |
| `Error/Message` | A description of the error. |

```xml
<?xml version="1.0" encoding="UTF-8"?>
<DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Deleted>
    <Key>photos/2024/cat.jpg</Key>
    <DeleteMarker>true</DeleteMarker>
    <DeleteMarkerVersionId>DeleteMarkerVersionId</DeleteMarkerVersionId>
  </Deleted>
  <Deleted>
    <Key>photos/2024/dog.jpg</Key>
    <VersionId>VersionId</VersionId>
  </Deleted>
</DeleteResult>
```

## Errors

These errors answer the request as a whole, and nothing in it is deleted:

| Code | HTTP status | When |
| --- | --- | --- |
| `MalformedXML` | 400 | The body is not a valid `Delete` document, has no `Object`, more than 1,000, or an empty `Key`. |
| `MaxMessageLengthExceeded` | 400 | The body is larger than 2 MiB. |
| `InvalidRequest` | 400 | Neither `Content-MD5` nor an `x-amz-checksum-*` header was sent. |
| `BadDigest` | 400 | `Content-MD5` does not match the body. |
| `XAmzContentSHA256Mismatch` | 400 | `x-amz-content-sha256` does not match the body. |
| `NotImplemented` | 501 | The request carries `x-amz-mfa`, or an entry carries `ETag`, `LastModifiedTime` or `Size`. |
| `AccessDenied` | 403 | The access key is not valid for the bucket, a key is outside its object prefix, or it lacks a permission an entry needs - see *Permissions* above. |
| `NoSuchBucket` | 404 | There is no bucket of that name of yours in the endpoint's region - it does not exist, is in another region, or your account has nothing stored in this endpoint's region yet. |
| `InvalidAccessKeyId` | 403 | The access key does not exist, has expired or been revoked, or it was created moments ago and is not yet usable in this endpoint's region, where you already store data. It becomes usable shortly after. |
| `SignatureDoesNotMatch` | 403 | The request signature is wrong. |
| `RequestTimeTooSkewed` | 403 | The request time is more than 15 minutes from the server's. |
| `AuthorizationHeaderMalformed` | 400 | The signature's credential scope names a region other than the endpoint's. |
| `SlowDown` | 503 | Too many requests at once. The AWS CLI and SDKs retry after the `Retry-After` interval. |
| `ServiceUnavailable` | 503 | A temporary failure. The AWS CLI and SDKs retry it on their own. |

An entry can also fail on its own, while the others go through. It is then reported in an
`Error` element of the response, with its own code and message. A version under retention or a
legal hold, for example, stays until the lock ends - [Object Lock](https://docs.strg.com/object-locks) explains
how locks work.

## Differences from Amazon S3

| Aspect | Amazon S3 | STRG |
| --- | --- | --- |
| Versioning | Deleting a key in an unversioned bucket removes the object | Every bucket is versioned: a key without a version ID writes a delete marker, and earlier versions stay billed as stored data |
| Access key limited to a prefix | Evaluated per object by the policy | Checked for every key before anything is deleted: a key outside the prefix answers the whole request with `403 AccessDenied` |
| Permissions | `s3:DeleteObject`, `s3:DeleteObjectVersion` evaluated per object | A version ID in any entry needs **Delete object versions**, checked up front for the whole request |
| Keys with `.` or `..` segments | Addressable by every operation | `DeleteObjects` is the way to delete them, because the key travels in the body rather than the URL |
| `ETag`, `LastModifiedTime`, `Size` in an entry | Conditional delete | `501 NotImplemented` |
| `x-amz-mfa` | MFA delete | `501 NotImplemented` |
| XML parsing | Lenient | Strict, so every key is read as meant: comments, `CDATA`, `DOCTYPE` and namespace prefixes are `400 MalformedXML` |
| Request body | - | Up to 2 MiB |
| Bucket in another region | `301` redirect with `x-amz-bucket-region` | `404 NoSuchBucket`. The bucket's own regional endpoint reaches it. |
| `x-amz-expected-bucket-owner` | Checked against the bucket owner | Welcome, with nothing to check |

## Examples

Writing delete markers for two objects:

**AWS CLI**

```bash
aws s3api delete-objects \
  --bucket my-bucket \
  --delete '{"Objects": [{"Key": "photos/2024/cat.jpg"}, {"Key": "photos/2024/dog.jpg"}]}' \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import boto3

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

response = s3.delete_objects(
    Bucket="my-bucket",
    Delete={
        "Objects": [
            {"Key": "photos/2024/cat.jpg"},
            {"Key": "photos/2024/dog.jpg"},
        ]
    },
)

for deleted in response.get("Deleted", []):
    print("deleted", deleted["Key"], deleted.get("DeleteMarkerVersionId"))
for error in response.get("Errors", []):
    print("failed", error["Key"], error["Code"], error["Message"])
```

**JavaScript (AWS SDK v3)**

```javascript
import { DeleteObjectsCommand, S3Client } from "@aws-sdk/client-s3";

const client = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
});

const { Deleted = [], Errors = [] } = await client.send(
  new DeleteObjectsCommand({
    Bucket: "my-bucket",
    Delete: {
      Objects: [{ Key: "photos/2024/cat.jpg" }, { Key: "photos/2024/dog.jpg" }],
    },
  }),
);

for (const deleted of Deleted) console.log("deleted", deleted.Key, deleted.DeleteMarkerVersionId);
for (const error of Errors) console.log("failed", error.Key, error.Code, error.Message);
```

Permanently deleting every version of `photos/2024/cat.jpg`, reporting only the entries that
fail. The version IDs come from [ListObjectVersions](https://docs.strg.com/s3-api/ListObjectVersions), and the access
key needs **Delete object versions**.

**Danger:**

A version deleted by its version ID is gone for good and cannot be restored.

**AWS CLI**

```bash
aws s3api list-object-versions \
  --bucket my-bucket \
  --prefix photos/2024/cat.jpg \
  --query '{Objects: [Versions[?Key==`"photos/2024/cat.jpg"`], DeleteMarkers[?Key==`"photos/2024/cat.jpg"`]][][].{Key: Key, VersionId: VersionId}, Quiet: `true`}' \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1 > delete.json

aws s3api delete-objects \
  --bucket my-bucket \
  --delete file://delete.json \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import boto3

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

key = "photos/2024/cat.jpg"
listing = s3.list_object_versions(Bucket="my-bucket", Prefix=key)
objects = [
    {"Key": entry["Key"], "VersionId": entry["VersionId"]}
    for entry in listing.get("Versions", []) + listing.get("DeleteMarkers", [])
    if entry["Key"] == key
]

response = s3.delete_objects(
    Bucket="my-bucket",
    Delete={"Objects": objects, "Quiet": True},
)
for error in response.get("Errors", []):
    print("failed", error["Key"], error["VersionId"], error["Code"], error["Message"])
```

**JavaScript (AWS SDK v3)**

```javascript
import { DeleteObjectsCommand, ListObjectVersionsCommand, S3Client } from "@aws-sdk/client-s3";

const client = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
});

const key = "photos/2024/cat.jpg";
const { Versions = [], DeleteMarkers = [] } = await client.send(
  new ListObjectVersionsCommand({ Bucket: "my-bucket", Prefix: key }),
);
const objects = [...Versions, ...DeleteMarkers]
  .filter((entry) => entry.Key === key)
  .map(({ Key, VersionId }) => ({ Key, VersionId }));

const { Errors = [] } = await client.send(
  new DeleteObjectsCommand({
    Bucket: "my-bucket",
    Delete: { Objects: objects, Quiet: true },
  }),
);
for (const error of Errors) console.log("failed", error.Key, error.VersionId, error.Code, error.Message);
```

## Related

- [DeleteObject](https://docs.strg.com/s3-api/DeleteObject)
- [ListObjectVersions](https://docs.strg.com/s3-api/ListObjectVersions)
- [PutObjectRetention](https://docs.strg.com/s3-api/PutObjectRetention), [PutObjectLegalHold](https://docs.strg.com/s3-api/PutObjectLegalHold)
- [Object Lock](https://docs.strg.com/object-locks)
- [Lifecycle policies](https://docs.strg.com/lifecycle-policies)
- [Access keys](https://docs.strg.com/access-keys)
- [S3 API overview](https://docs.strg.com/s3-api/overview)
