# Objects app API

> The Objects API your backend calls on your app's own origin: list objects, get download links, upload in parts, abort or complete an upload, and delete.
> Source: https://mdrn.io/docs/reference/app/objects/

Your backend calls every route below on your app's own origin, through `env.MDRN_PRODUCTS.fetch`, with `Authorization: Bearer` and the credential in `MDRN_OBJECTS_TOKEN`; the address is in `MDRN_OBJECTS_URL`. A browser never calls them: a call without the credential is answered `404 CALL_FROM_BACKEND`. Every call that is not a `GET` sends an `Idempotency-Key` (1 to 128 characters, new for each change, the same when you retry it); a call that edits a versioned resource sends `If-Match` with the version you read. [Apps with a backend](/docs/apps-with-a-backend/) says where the variables come from.

Every error is JSON with a code you can test for: [Error codes](/docs/errors/) lists them. The [API reference](/docs/reference/) says how these pages fit together.

## Objects

### GET /v1/objects

**List objects**

The environment's objects, ordered by id, in pages of 1 to 200 (default 50): ready ones unless state says otherwise. state=deleted lists objects deleted in the last 30 days and takes a management token; your app's credential cannot list them. state=lost lists objects MDRN could no longer find when it archived or restored the environment; they cannot be downloaded. A management token can list objects while Objects is switched off or archived, to export them; your app's credential cannot.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:read` | Your backend's product credential, or a management token | Safe to repeat |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `cursor` | query | no | The nextCursor of the previous page, with the same filter. Read for 24 hours. |
| `limit` | query | no | 1 to 200; 50 when left out. Type: integer (at least 1, at most 200, default `50`). |
| `state` | query | no | ready (the default), deleted or lost. Type: one of `"ready"`, `"deleted"`, `"lost"`. |

**Response** `200` `application/json` The objects.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of [ObjectsObject](#objectsobject) | yes |  |
| `nextCursor` | string, or null | yes |  |

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

### GET /v1/objects/{objectId}

**Read an object**

The object: its name, type, size, SHA-256 and state. A deleted object is answered to a management token only. A management token can read an object while Objects is switched off or archived; your app's credential cannot.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:read` | Your backend's product credential, or a management token | Safe to repeat |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `objectId` | path | yes | The object's id. |

**Response** `200` `application/json` The object.

[ObjectsObject](#objectsobject)

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

### DELETE /v1/objects/{objectId}

**Delete an object**

Every link to the object stops working at once and it leaves the list. The object is kept 30 days, counting as stored, in which a management token can restore it; deleting it again does not extend the 30 days.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:write` | Your backend's product credential, or a management token | `Idempotency-Key` required |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `objectId` | path | yes | The object's id. |

**Response** `204`: no body. Deleted.

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

### POST /v1/objects/{objectId}/download-grants

**Get a download link**

Answers a link (url) whoever holds it can GET for 15 minutes, as often as they like, with a Range header for part of the object, and no credential. For a ready object only. The link stops working when the object is deleted or Objects is switched on or off. While Objects is switched off or archived (an unpaid workspace, during its 90 days), a management token still gets links, so every object can be exported; your app's credential cannot, nothing is restored or moved by reading, and downloads through those links are not charged.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:read` | Your backend's product credential, or a management token | `Idempotency-Key` required |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `objectId` | path | yes | The object's id. |

**Response** `200` `application/json` The link, with its method, headers and expiry.

[ObjectsGrant](#objectsgrant)

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

## Uploads

### GET /v1/objects/uploads

**List uploads**

The environment's uploads, in pages of 1 to 200 (default 50); a cursor works for 24 hours.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:read` | Your backend's product credential, or a management token | Safe to repeat |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `cursor` | query | no | The nextCursor of the previous page, with the same filter. Read for 24 hours. |
| `limit` | query | no | 1 to 200; 50 when left out. Type: integer (at least 1, at most 200, default `50`). |

**Response** `200` `application/json` The uploads.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of [ObjectsUpload](#objectsupload) | yes |  |
| `nextCursor` | string, or null | yes |  |

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

### POST /v1/objects/uploads

**Start an upload**

Opens an upload for one object, with its name, type, size and the SHA-256 of the whole object, which the bytes are checked against before the object becomes ready. Answers 202 with the upload, which MDRN prepares for up to about 5 seconds first, so its state is almost always uploading already; if it is not yet, read it until it is, then send the parts. An upload not completed within 24 hours expires. A project and environment may have at most 100 uploads open at once (429 TOO_MANY_OPEN_UPLOADS) and hold at most 1 TiB, counting what open uploads reserve and deleted objects until they are removed (409 OBJECTS_QUOTA_EXCEEDED). In build mode one object may be at most 100 MB and a project and environment may hold at most 1 GB, counted the same way (402 BUILD_LIMIT_REACHED); nothing is deleted, and going live lifts both.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:write` | Your backend's product credential, or a management token | `Idempotency-Key` required |

**Request body** `application/json`

[ObjectsUploadRequest](#objectsuploadrequest)

**Response** `202` `application/json` The upload, usually uploading already; if not yet, read it until it is.

[ObjectsUpload](#objectsupload)

**Errors**: `400`, `401`, `402`, `403`, `404`, `409`, `413`, `429`, `503`, each JSON with a code: [Error codes](/docs/errors/).

### GET /v1/objects/uploads/{uploadId}

**Read an upload**

The upload: its state, when it expires, partBytes and, once completed, the operationId of the check. Read it to resume an upload after an interruption; the parts already recorded are listed under /parts.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:read` | Your backend's product credential, or a management token | Safe to repeat |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `uploadId` | path | yes | The upload's id. |

**Response** `200` `application/json` The upload.

[ObjectsUpload](#objectsupload)

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

### DELETE /v1/objects/uploads/{uploadId}

**Abort an upload**

Ends the upload: its part links stop working and what was sent is removed. An upload never completed leaves no object.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:write` | Your backend's product credential, or a management token | `Idempotency-Key` required |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `uploadId` | path | yes | The upload's id. |

**Response** `204`: no body. Aborted.

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

### POST /v1/objects/uploads/{uploadId}/complete

**Complete an upload**

Names every ready part with its number and exact etag, or an empty array for a zero-byte object, and answers 202 with the object and the operationId of the check. MDRN checks the bytes for up to about 5 seconds before it answers, so a small object is usually ready already; while it is verifying, read the object until its state is ready; one whose bytes do not match the SHA-256 or the size is rejected and never served.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:write` | Your backend's product credential, or a management token | `Idempotency-Key` required |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `uploadId` | path | yes | The upload's id. |

**Request body** `application/json`

[ObjectsCompleteRequest](#objectscompleterequest)

**Response** `202` `application/json` The object: ready, rejected, or still verifying.

[ObjectsCompletion](#objectscompletion)

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

### GET /v1/objects/uploads/{uploadId}/parts

**List the parts of an upload**

The parts recorded so far, each with its number, size, etag and state. Only ready parts can be named when completing, and a part's number cannot change once it is recorded.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:read` | Your backend's product credential, or a management token | Safe to repeat |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `uploadId` | path | yes | The upload's id. |
| `cursor` | query | no | The nextCursor of the previous page, with the same filter. Read for 24 hours. |
| `limit` | query | no | 1 to 200; 50 when left out. Type: integer (at least 1, at most 200, default `50`). |

**Response** `200` `application/json` The parts.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of [ObjectsPart](#objectspart) | yes |  |
| `nextCursor` | string, or null | yes |  |

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

### POST /v1/objects/uploads/{uploadId}/parts

**Get a link to upload one part**

Answers 202 with a link (url, method PUT, headers) to send one part's bytes to, with partNumber from 1 and the part's size. Every part is 16 MiB except the last, which may be shorter. A new link can be asked for while the upload is open, for a part not sent yet or one to send again.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:write` | Your backend's product credential, or a management token | `Idempotency-Key` required |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `uploadId` | path | yes | The upload's id. |

**Request body** `application/json`

[ObjectsPartRequest](#objectspartrequest)

**Response** `202` `application/json` The link to PUT the part to.

[ObjectsGrant](#objectsgrant)

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

## Operations

### GET /v1/objects/operations/{operationId}

**Read an operation**

The operation a 202 answered, with its state: accepted, running and retrying are unfinished; failed carries an error code you can act on.

| Scope | Who | Idempotency |
| --- | --- | --- |
| `objects:read` | Your backend's product credential, or a management token | Safe to repeat |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `operationId` | path | yes | The operation's id, from the answer that started it. |

**Response** `200` `application/json` The operation.

[ObjectsOperation](#objectsoperation)

**Errors**: `400`, `401`, `403`, `404`, `409`, `413`, `503`, each JSON with a code: [Error codes](/docs/errors/).

## Other

### GET /transfer/{grant}

**Download an object**

The address in a download link: whoever holds it GETs the object, with no credential, until the link expires; a Range header asks for part of it. The link is for one object and the one method it was issued for, and stops working when the object is deleted or Objects is switched on or off for the environment; a link a management token got while Objects was off works until then.

| Scope | Who | Idempotency |
| --- | --- | --- |
| none | Whoever holds the transfer link | Safe to repeat |

**Parameters**

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `Range` | header | no | One bytes=start-end, bytes=start- or bytes=-suffix range. A malformed Range, another unit or several ranges are ignored and the whole object is sent (200). |
| `If-Range` | header | no | The range is served only while this strong entity tag is the object's ETag; otherwise (a weak tag or a date) the whole object is sent. |

**Response** `200`: no body. The object's bytes.

**Response** `206`: no body. The byte range asked for, with Content-Range.

**Errors**: `400`, `403`, `404`, `409`, `416`, each JSON with a code: [Error codes](/docs/errors/).

### PUT /transfer/{grant}

**Upload one part**

The address in a part link: PUT exactly the part's bytes, with the headers the link came with, and read the part's etag from the answer. The link is for one upload, one part and one size, until it expires; a body of another size is 400.

| Scope | Who | Idempotency |
| --- | --- | --- |
| none | Whoever holds the transfer link | No `Idempotency-Key` |

**Request body** `application/octet-stream`

string (binary)

**Response** `200`: no body. Part recorded; the answer carries partNumber, bytes and etag.

**Errors**: `400`, `403`, `404`, `409`, each JSON with a code: [Error codes](/docs/errors/).

## Schemas

### ObjectsCompleteRequest

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `parts` | array of object with `partNumber`, `etag` (at most 640 items) | yes | Every part, with the etag its upload answered; empty for a zero-byte object. |

### ObjectsCompletion

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | The object's id. |
| `filename` | string | yes |  |
| `contentType` | string | yes |  |
| `bytes` | integer (int64) (at least 0) | yes |  |
| `state` | one of `"verifying"`, `"ready"`, `"rejected"` | yes | Where the check had got to when the call answered: usually ready (or rejected) for a small object; verifying while it goes on. |
| `operationId` | string | yes | The check to follow; the object is ready once it succeeds. |

### ObjectsGrant

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `url` | string (uri) | yes | The address to send the request to, with exactly the method and headers given; the whole URL is a credential. |
| `method` | one of `"GET"`, `"PUT"` | yes |  |
| `headers` | empty object | yes | Headers to send with the request. |
| `expiresAt` | string (date-time) | yes | The link stops working then, 15 minutes after it was issued. |
| `maxBytes` | integer (int64) (at least 0) | yes |  |

### ObjectsObject

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `filename` | string | yes |  |
| `contentType` | string | yes |  |
| `bytes` | integer (int64) (at least 0) | yes |  |
| `sha256` | string | yes |  |
| `state` | one of `"verifying"`, `"ready"`, `"rejected"`, `"deleted"`, `"purging"`, `"purge_failed"`, `"purged"`, `"lost"` | yes | ready: verified and served. rejected: the bytes did not match the SHA-256 or the size; never served. deleted: kept until purgeAt and restorable until then. lost: MDRN could no longer find the object when it archived or restored the environment; it cannot be downloaded. purge_failed: the object's permanent removal gave up; it is retried three more times, a day apart, and again when every object in the environment is next deleted. A deleted object stops counting as stored when its permanent removal begins, whether or not that succeeds. |
| `createdAt` | string (date-time) | yes |  |
| `deletedAt` | string (date-time), or null | yes |  |
| `purgeAt` | string (date-time), or null | yes |  |

### ObjectsOperation

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `kind` | string | yes |  |
| `state` | string | yes | accepted, running and retrying are unfinished; succeeded and failed are final. |
| `processedBytes` | integer (int64) (at least 0) | yes |  |
| `processedItems` | integer (int64) (at least 0) | yes |  |
| `attempts` | integer (int64) (at least 0) | yes |  |
| `retryAt` | string (date-time), or null | yes |  |
| `error` | string, or null | yes |  |

### ObjectsPart

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `partNumber` | integer (at least 1, at most 640) | yes |  |
| `bytes` | integer (int64) (at least 0) | yes |  |
| `etag` | string, or null | yes |  |
| `state` | string | yes |  |

### ObjectsPartRequest

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `partNumber` | integer (at least 1, at most 640) | yes |  |
| `bytes` | integer (int64) (at least 1, at most 16777216) | yes | The part's size: 16 MiB for every part but the last. |

### ObjectsUpload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `objectId` | string | yes | The id the object will have. |
| `state` | one of `"pending"`, `"uploading"`, `"verifying"`, `"complete"`, `"aborted"`, `"expired"`, `"rejected"` | yes | uploading: send the parts. verifying: completed, and the bytes are being checked. expired: not completed within 24 hours. |
| `expiresAt` | string (date-time) | yes |  |
| `operationId` | string, or null | yes |  |
| `partBytes` | `16777216` | yes | The size of every part but the last, in bytes (16 MiB). |

### ObjectsUploadRequest

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `filename` | string (at least 1 character, at most 255 characters) | yes | The object's name, at most 255 UTF-8 bytes, no control characters and no invisible direction or zero-width characters (the zero-width joiner is allowed); shown and sent with downloads, never used as a path. |
| `contentType` | string (at least 1 character, at most 128 characters) | yes | The object's media type, at most 128 UTF-8 bytes, no control characters. |
| `bytes` | integer (int64) (at least 0, at most 10737418240) | yes | The object's size, at most 10 GiB. |
| `sha256` | string (matching `^[a-fA-F0-9]{64}$`) | yes | The hex SHA-256 of the whole object; the upload is rejected when the bytes do not match it. |
