# PutObjectRetention

PutObjectRetention sets the retention of an object version: its mode, governance or
compliance, and the date until which it is locked. Without `versionId` it sets the current
version's. Retention exists only in a bucket with [Object Lock](https://docs.strg.com/object-locks) enabled,
and reading one back is the job of [GetObjectRetention](https://docs.strg.com/s3-api/GetObjectRetention).

**Info: Good to know on STRG**

- Setting a retention takes the *Set retention* permission on the access key. Shortening,
  removing or changing a governance-mode lock takes *Bypass governance retention* as well.
- `x-amz-expected-bucket-owner` is welcome and has nothing to check: a key only ever reaches
  buckets of its own account.
- A key containing a `.` or `..` path segment (`a/../b`) cannot be put in a URL, so STRG
  answers `400 InvalidURI` for it.
- The body is sent whole: an `aws-chunked` body is answered with `501 NotImplemented`, and the
  AWS SDKs do not send this operation's body that way by default.

**Danger: A lock stays until its date**

A locked object version cannot be deleted permanently until its retention date has passed, and
it is billed as stored data until then. A **compliance-mode** lock cannot be shortened or
removed by anyone - it can only be extended. Once set, that version is kept, and billed, until
the date it names.

## When you'd use it

- **Keeping one backup longer.** A bucket's default retention locks every new backup for a few
  weeks; the backup taken at the end of a quarter can have its lock extended to a year, so it
  outlasts the rest.
- **Records that must be kept.** A system archiving invoices or contracts locks each one in
  compliance mode until the end of the period the law asks for, so nobody - an administrator
  included - can remove it early.
- **Protecting files after the fact.** An object uploaded before anyone thought to lock it can
  be given a retention later, version by version.
- **Correcting a mistake.** A governance-mode lock set further out than intended can be
  shortened by a key holding *Bypass governance retention* - the one way back
  [governance mode](https://docs.strg.com/object-locks#bypassing-governance-retention) leaves open.

## Request

```http
PUT /photos/2024/cat.jpg?retention 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, hex>
Content-MD5: <MD5 of the body, base64>
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<Retention xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Mode>GOVERNANCE</Mode>
  <RetainUntilDate>2027-01-01T00:00:00.000Z</RetainUntilDate>
</Retention>
```

The path-style form is `PUT /my-bucket/photos/2024/cat.jpg?retention` on
`s3.eu-central-1.strg.com`, which a bucket name containing a dot needs -
[Addressing](https://docs.strg.com/s3-api/overview#addressing) explains both.

### Permissions

Which permissions a request needs depends on the change it makes:

| Change | Permissions needed |
| --- | --- |
| Setting a retention on a version that has none, or extending one to a later date | **Set retention** (`object-retention:write`) |
| Moving a compliance-mode retention to a later date | **Set retention** (`object-retention:write`) |
| Shortening or removing a governance-mode retention, or changing it to compliance mode | **Set retention** and **Bypass governance retention** (`object-retention:bypass-governance`), with `x-amz-bypass-governance-retention: true` on the request |
| Shortening or removing a compliance-mode retention | No permission allows it: a compliance-mode retention only ever gets longer |

A request carrying `x-amz-bypass-governance-retention: true` from a key without *Bypass
governance retention* answers `403 AccessDenied`, whether or not the change needed it. For a key
limited to certain buckets or to a key prefix, the bucket and the object key are within them.
[Access keys](https://docs.strg.com/access-keys) has the details.

### URI parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Bucket` | String | Yes | The bucket holding the object. |
| `Key` | String | Yes | The object key. A key containing a `.` or `..` path segment answers `400 InvalidURI`. |
| `retention` | - | Yes | Selects this operation. Takes no value. |
| `versionId` | String | No | The version whose retention to set; without it, the current version's. A version ID is an opaque string. |

Any other query parameter is answered with `501 NotImplemented`, so a client knows straight
away. The `X-Amz-*` parameters of a presigned URL and the `x-id` parameter the JavaScript SDK
adds are welcome.

### Request headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `x-amz-bypass-governance-retention` | Boolean | No | `true` to shorten, remove or change a governance-mode retention. Takes *Bypass governance retention* on the access key. Has no effect on a compliance-mode retention. |
| `Content-MD5` | String | No | The base64 MD5 digest of the body. A digest that does not match the body answers `400 BadDigest`. The AWS SDKs send an integrity header by default. |
| `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 body, accepted in place of `Content-MD5`. |
| `x-amz-sdk-checksum-algorithm` | String | No | The algorithm of the `x-amz-checksum-*` header the SDK sends beside it. Accepted. |
| `x-amz-expected-bucket-owner` | String | No | Welcome, with nothing to check: an access key only ever reaches its own account's buckets. |
| `x-amz-request-payer` | String | No | Not applicable: STRG has no Requester Pays buckets. |

### Request body

A `Retention` document, UTF-8, up to 2 MiB, sent whole - an `aws-chunked` body is answered with
`501 NotImplemented`.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Retention xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Mode>COMPLIANCE</Mode>
  <RetainUntilDate>2027-01-01T00:00:00.000Z</RetainUntilDate>
</Retention>
```

| Element | Type | Required | Description |
| --- | --- | --- | --- |
| `Retention` | Container | Yes | The object version's new retention. |
| `Mode` | String | No | `GOVERNANCE` or `COMPLIANCE`. [How it works](https://docs.strg.com/object-locks#how-it-works) explains what each allows. |
| `RetainUntilDate` | Timestamp | No | The date and time, in UTC and ISO 8601 form, until which the version cannot be deleted permanently. |

## Response

A successful request answers `200 OK` with no body.

### Response headers

| Name | Description |
| --- | --- |
| `x-amz-request-id` | The request ID, handy for a support request. |
| `x-amz-request-charged` | Not returned: STRG has no Requester Pays buckets. |

### Response body

None.

## Errors

These are the errors this request can return, and the situation behind each one.

| Code | HTTP status | When |
| --- | --- | --- |
| `NotImplemented` | 501 | The request carries a query parameter other than `retention` and `versionId` (presigning's `X-Amz-*` parameters and `x-id` aside), or an `aws-chunked` body. |
| `NoSuchBucket` | 404 | The bucket does not exist, it is in a region other than the endpoint's, or the account has no storage in this endpoint's region. |
| `NoSuchKey` | 404 | The object does not exist. |
| `AccessDenied` | 403 | The access key lacks *Set retention*, or sends `x-amz-bypass-governance-retention: true` without *Bypass governance retention*, or the bucket or key is outside its buckets or prefix. |
| `InvalidAccessKeyId` | 403 | The access key does not exist, has expired or been revoked - or it was created moments ago and is not usable yet in a region where the account already stores data, in which case the same request works shortly afterwards. |
| `InvalidURI` | 400 | The key contains a `.` or `..` path segment. |
| `SignatureDoesNotMatch` | 403 | The request signature does not match. |
| `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. |
| `BadDigest` | 400 | `Content-MD5` does not match the body. |
| `XAmzContentSHA256Mismatch` | 400 | `x-amz-content-sha256` does not match the body. |
| `MalformedXML` | 400 | The body is not valid UTF-8, or not a valid `Retention` document. |
| `MaxMessageLengthExceeded` | 400 | The body is larger than 2 MiB. |
| `SlowDown` | 503 | Too many requests. The `Retry-After` header says when a retry is welcome; the AWS SDKs retry on their own. |
| `ServiceUnavailable` | 503 | A temporary failure; the same request can be retried. |
| `InternalError` | 500 | An unexpected failure; the same request can be retried. |

A request that would shorten or remove a compliance-mode retention, that would shorten, remove
or change a governance-mode retention without `x-amz-bypass-governance-retention: true`, or that
names an object in a bucket without Object Lock, answers a `4xx` error and changes nothing - the
existing lock stays exactly as it was.

## Differences from Amazon S3

The table lists each place where STRG behaves differently, and what that means for a client.

| Aspect | Amazon S3 | STRG |
| --- | --- | --- |
| Permissions | `s3:PutObjectRetention`, plus `s3:BypassGovernanceRetention` to bypass | *Set retention* (`object-retention:write`), plus *Bypass governance retention* (`object-retention:bypass-governance`) on the access key |
| `x-amz-expected-bucket-owner` | Checked against the bucket owner | Welcome, with nothing to check |
| Bucket in another region | `301` redirect with `x-amz-bucket-region` | `404 NoSuchBucket`. A bucket is reached through its own regional endpoint |
| Nothing stored in the endpoint's region yet | Any valid key reaches the endpoint | `404 NoSuchBucket`, since the account has no buckets there |
| Keys with `.` or `..` segments | Allowed | `400 InvalidURI`, because such a key cannot be put in a URL |
| `aws-chunked` request body | Accepted | `501 NotImplemented`. The SDKs send this operation's body whole by default, so it does not come up in everyday use |

## Examples

Locking the current version of `photos/2024/cat.jpg` in governance mode until 1 January 2027:

**AWS CLI**

```bash
aws s3api put-object-retention \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --retention '{"Mode":"GOVERNANCE","RetainUntilDate":"2027-01-01T00:00:00Z"}' \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
from datetime import datetime, timezone

import boto3

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

s3.put_object_retention(
    Bucket="my-bucket",
    Key="photos/2024/cat.jpg",
    Retention={
        "Mode": "GOVERNANCE",
        "RetainUntilDate": datetime(2027, 1, 1, tzinfo=timezone.utc),
    },
)
```

**JavaScript (AWS SDK v3)**

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

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

await client.send(
  new PutObjectRetentionCommand({
    Bucket: "my-bucket",
    Key: "photos/2024/cat.jpg",
    Retention: {
      Mode: "GOVERNANCE",
      RetainUntilDate: new Date("2027-01-01T00:00:00Z"),
    },
  }),
);
```

Shortening the governance-mode retention of a specific version, which takes *Bypass governance
retention* on the access key, with `VERSION_ID` set to a version ID from
[ListObjectVersions](https://docs.strg.com/s3-api/ListObjectVersions):

**AWS CLI**

```bash
aws s3api put-object-retention \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --version-id "$VERSION_ID" \
  --retention '{"Mode":"GOVERNANCE","RetainUntilDate":"2026-10-31T00:00:00Z"}' \
  --bypass-governance-retention \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import os
from datetime import datetime, timezone

import boto3

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

s3.put_object_retention(
    Bucket="my-bucket",
    Key="photos/2024/cat.jpg",
    VersionId=os.environ["VERSION_ID"],
    Retention={
        "Mode": "GOVERNANCE",
        "RetainUntilDate": datetime(2026, 10, 31, tzinfo=timezone.utc),
    },
    BypassGovernanceRetention=True,
)
```

**JavaScript (AWS SDK v3)**

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

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

await client.send(
  new PutObjectRetentionCommand({
    Bucket: "my-bucket",
    Key: "photos/2024/cat.jpg",
    VersionId: process.env.VERSION_ID,
    Retention: {
      Mode: "GOVERNANCE",
      RetainUntilDate: new Date("2026-10-31T00:00:00Z"),
    },
    BypassGovernanceRetention: true,
  }),
);
```

## Related

- [GetObjectRetention](https://docs.strg.com/s3-api/GetObjectRetention)
- [PutObjectLegalHold](https://docs.strg.com/s3-api/PutObjectLegalHold)
- [PutObjectLockConfiguration](https://docs.strg.com/s3-api/PutObjectLockConfiguration)
- [DeleteObject](https://docs.strg.com/s3-api/DeleteObject)
- [ListObjectVersions](https://docs.strg.com/s3-api/ListObjectVersions)
- [Object Lock](https://docs.strg.com/object-locks)
- [Access keys](https://docs.strg.com/access-keys)
- [S3 API overview](https://docs.strg.com/s3-api/overview)
