HTTP Storage Protocol
HTTP Storage Protocol v1 defines requests and responses between the Office HTTP Storage adapter and an HTTP Storage Provider that operates its own storage service. The Provider implements the endpoints below, and Office sends requests to them. See HTTP Storage for connection instructions and examples.
This document defines the methods and paths, request headers and bodies, response fields, and status codes for all nine endpoints. Common authentication, path, and size rules apply to every endpoint. Additional conditions appear in each operation's section.
URLs and paths
{PROVIDER_BASE_URL}/tfo-storage/v1/{ENCODED_DOCUMENT_PATH}/{OPERATION}
The Provider base URL is an HTTP or HTTPS address and may include a path prefix. User information, query strings, fragments, and . or .. path segments are not allowed. Remove any trailing / before appending the protocol path.
Document paths are relative to the Provider root. Percent-encode each UTF-8 path segment separately and join them with /. The root has an empty document path, as in /tfo-storage/v1/list. Do not insert empty intermediate segments or extra / characters.
| Item | Example |
|---|---|
| Provider base URL | https://storage.example.com/office |
| Document path | contracts/Proposal 2026.docx |
| Document information request | GET /office/tfo-storage/v1/contracts/Proposal%202026.docx/info |
| Root listing request | GET /office/tfo-storage/v1/list |
The Provider must resolve decoded paths only within its storage root. Reject . and .., / or \ inside path segments, control characters, invalid UTF-8, and symbolic links that escape the root. The protocol does not use query parameters, cookies, redirects, or forwarding of arbitrary authentication headers. Proxies must preserve the raw encoded path used for JWT verification.
Common request headers
| Header | Scope | Value |
|---|---|---|
X-TFO-Storage-Adapter | Required on every request | The registered adapter name: 1–128 alphanumeric, ., _, or - characters, beginning with an alphanumeric character |
X-TFO-Storage-Request-JWT | Required on every request | The raw JWT signed according to the rules below, without a Bearer prefix |
Content-Type | put, lock, unlock, mkdir, rename | application/octet-stream for put; application/json for JSON operations |
Content-Length | Required on requests with a body | A single decimal integer giving the exact body length in bytes |
Requests for info, list, get, and delete have no body or Content-Type. Sign them with body length 0 and the SHA-256 digest of an empty body. Do not wrap bodies in multipart or base64, or use chunked uploads or compression. For JSON bodies, sign the actual UTF-8 serialized bytes.
JWT signing and verification
Before reading or modifying a document, the Provider must verify that the request was signed with the connected Office adapter's secret and that its method, path, and body were not changed in transit. Office signs a JWT containing request information with the shared secret. The Provider verifies the signature with the same secret and compares it with the actual request. It also checks the short validity period and unique request ID to reject replayed requests.
Signatures protect request integrity; they do not encrypt document contents. Use HTTPS to protect data in transit, and separately enforce user and tenant document access permissions in the Provider.
Office and the Provider share a random secret of at least 32 UTF-8 bytes per adapter. The HMAC key is the UTF-8 byte representation of this string. Never send the secret in request URLs or bodies.
The JWT uses compact JWS format. Serialize the header and payload separately as UTF-8 JSON and Base64URL-encode them without padding. Join them with ., apply HMAC-SHA256 to those bytes, and append the signature using the same Base64URL encoding.
{"alg":"HS256","typ":"tfo-storage-request+jwt"}
The following payload example signs a request that saves the five-byte body hello. Generate fresh iat, exp, and jti values for every call.
{
"iss": "thinkfree-office",
"aud": "tfo-http-storage-provider",
"iat": 1789344000,
"exp": 1789344060,
"jti": "26bc9b5e-9dab-4a78-95d8-b1ae34a5d9eb",
"request": {
"adapter": "customer-storage-a",
"method": "PUT",
"path": "/tfo-storage/v1/contracts/hello.txt/put",
"content_type": "application/octet-stream",
"content_length": 5,
"content_sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"office_connection_id": "example-connection",
"arguments": {"save_type": "save"},
"client_metadata": {"project": "example-project"}
}
}
| Field | Type | Required | Validation rule |
|---|---|---|---|
Header alg | string | Yes | Exactly HS256 |
Header typ | string | Yes | Exactly tfo-storage-request+jwt |
iss | string | Yes | Exactly thinkfree-office |
aud | string or single-element array | Yes | The sole audience is tfo-http-storage-provider |
iat | integer | Yes | Issued-at Unix time in seconds, not later than the current time |
exp | integer | Yes | Expiration Unix time in seconds, later than the current time, with 0 < exp - iat <= 60 |
jti | string | Yes | A unique ID for each request. Office generates a UUID; the public examples accept 1–64 characters |
request | object | Yes | Contains the signed request fields below |
request.adapter | string | Yes | Matches both the actual adapter header and the connection name configured in the Provider |
request.method | string | Yes | Matches the actual uppercase HTTP method |
request.path | string | Yes | Matches the actual raw encoded path, including the Provider base path, without scheme, host, or query |
request.content_type | string | When the header is present | Exactly matches the actual Content-Type |
request.content_length | integer | Yes | The actual number of body bytes received, or 0 when there is no body |
request.content_sha256 | string | Yes | SHA-256 of the actual body bytes, as 64 lowercase hexadecimal characters |
request.office_connection_id | string | No | Opaque Office connection context; not proof of user authentication |
request.arguments | object | No | Operation context such as save_type; does not grant access |
request.client_metadata | object | No | Caller-provided JSON. Only transport integrity is guaranteed; do not trust it as an Office-verified user identity |
The SHA-256 digest of an empty body is:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
The complete JWT must not exceed 5,120 bytes. client_metadata is limited to 2,048 bytes of UTF-8 JSON. Object keys must contain 1–64 characters and must not be blank; string values are limited to 512 characters. The maximum depth is 8, counting the root object as depth 0. Do not include secrets or document contents.
Verification order
- Check the actual HTTP method, raw path, presence of a query, media type, and fixed length. Reject oversized bodies before reading or buffering them.
- Read allowed bodies into bounded memory or a temporary file while calculating their actual length and SHA-256 digest. Do not replace the target document before the upload completes.
- Look up the configured secret using the adapter header, then verify the JWT signature, header, issuer, audience, and times.
- Compare the signed adapter, method, raw path, body type, length, and hash with the actual request.
- Record
jtiatomically and reject reuse untilexp. Multiple Provider instances must share the same replay-protection store. - Check document access permissions and perform the operation, or return the exact unsupported response for an optional operation.
Reject invalid signatures and reused JWTs before accessing storage or reporting unsupported operations. Signature verification does not replace the Provider's user and tenant document authorization checks. Synchronize server clocks and sign retries with fresh iat, exp, and jti values. jti is not an idempotency key that returns the successful result of a duplicate operation. If a response is lost, check the storage state before retrying according to the operation's semantics.
Response framing and size limits
Successful info, list, get, and put responses must have exactly one decimal Content-Length header matching the number of bytes actually sent. Transfer-Encoding and Content-Encoding are not allowed. A charset parameter in the media type is allowed, but JSON bodies must be UTF-8.
| Target | Content-Type | Limit |
|---|---|---|
Successful info, list, and put responses | application/json | 5 MiB each (5,242,880 bytes) |
Successful get response | application/octet-stream | 300 MiB (314,572,800 bytes) |
put request | application/octet-stream | 300 MiB (314,572,800 bytes) |
File size in metadata | JSON integer | 0–314,572,800 |
entries in list | JSON array | At most 10,000 entries, with the complete JSON also limited to 5 MiB |
Serialize JSON once, calculate its byte length, and send those exact bytes. For get, obtain the original file size from storage before sending response headers, then stream exactly that many bytes. Do not load an entire document into memory just to determine its size.
A Provider may impose a lower document limit but must not exceed 300 MiB. Check the limit before processing file metadata, sending get, or buffering put. Limits are inclusive: JSON of 5,242,880 bytes and documents of 314,572,800 bytes are allowed, while 5,242,881 and 314,572,801 bytes respectively must be rejected.
Missing, duplicate, negative, or fractional lengths; compressed or chunked responses; incorrect media types; early EOF; and actual length mismatches fail the current operation. The Provider must remain able to process subsequent requests after Office rejects an invalid response. Authenticated 501 responses and ordinary errors follow the separate rules below.
Success statuses and bodies
| Operation | Normal response | Body contract |
|---|---|---|
info, list | 200 OK | A JSON object is required; do not substitute an empty body or 204 |
get | 200 OK | The complete file bytes; return an empty file with Content-Length: 0 |
put | 200 OK | A JSON object with docId is required. See PUT response |
lock, unlock, mkdir, rename, delete | 204 No Content | No body |
Office classifies 2xx statuses as successful, but info, list, get, and put must also pass the media type, fixed-length, and body checks above. lock, unlock, mkdir, rename, and delete also accept other 2xx statuses and require no specific response JSON fields. Never send a body with 204. If these five operations return result text in a 200 response, use text/plain; charset=utf-8 and keep it within the 10 MiB limit Office reads. Do not return document contents in that result text.
Metadata fields
info returns one entry object; list returns one object containing an entries array. Do not add fields other than those below to entry objects.
| Field | Type | Required | Meaning and limits |
|---|---|---|---|
path | string | Yes | Decoded path relative to the root, at most 4,096 characters. The root uses an empty string |
name | string | Yes | A 1–255 character name matching the final path segment. The root uses a fixed display name |
type | string | Yes | file or directory |
size | integer | Yes | File length in bytes, 0–314,572,800. Must be 0 for directories |
readable | boolean | Yes | Whether the document can be read |
writable | boolean | Yes | Whether the document can be written |
locked | boolean | Yes | Current lock state |
locker | string or null | No | Lock owner, at most 255 characters |
createdAt | string or null | No | RFC 3339 creation time, at most 64 characters |
modifiedAt | string or null | No | RFC 3339 modification time, at most 64 characters |
revision | string or null | No | Storage revision or ETag-equivalent value, at most 1,024 characters |
info.path must match the requested document path. list must contain only the requested directory's immediate children, with no duplicate paths. There are no pagination parameters. Invalid types, timestamps, names, and sizes; unknown fields; duplicate paths; and entries from inside subdirectories are rejected.
Required fields must not be null. Optional fields may be omitted or returned as null. path is decoded, not URL-encoded. createdAt and modifiedAt include a time zone, as in 2026-09-14T00:00:00Z. readable and writable describe an entry's accessibility; continue to enforce authorization on actual requests. revision describes the storage version and is not automatically used as a conditional request header or lock token in this protocol.
For machine validation, use the entry schema, info response schema, and list response schema. Also check the path relationships and runtime limits above in addition to schema validation.
Requests and responses by operation
path identifies the target entry, and parent identifies the parent for a new directory. Both are relative paths under the Provider root. Encode each segment according to the URL rules, rather than encoding the entire path. Every request requires the common authentication headers, X-TFO-Storage-Adapter and X-TFO-Storage-Request-JWT, and has no query parameters.
JSON requests are objects containing only the single field specified below. Required fields cannot be omitted, null, or another type, and extra fields are not allowed. The request Content-Type must be exactly application/json, and Content-Length is the length of the UTF-8 serialized bytes. The public examples limit JSON requests to 16 KiB, separately from the 5 MiB response limit.
Each operation's error table describes its specific conditions. The common errors for paths, authentication, permissions, body formats, and storage failures also apply to every endpoint. Office does not require every Provider to use an identical ordinary error body. Conflict and filesystem policies specific to the public examples are identified separately.
| Operation | Method | Path suffix | Implementation |
|---|---|---|---|
| info | GET | /{path}/info | Required |
| list | GET | /{path}/list | Optional |
| get | GET | /{path}/get | Required |
| put | PUT | /{path}/put | Optional |
| lock | POST | /{path}/lock | Optional together with unlock |
| unlock | POST | /{path}/unlock | Optional together with lock |
| mkdir | POST | /{parent}/mkdir | Optional |
| rename | POST | /{path}/rename | Optional |
| delete | DELETE | /{path}/delete | Optional |
The examples below use base URL https://storage.example.com. Replace <SIGNED_REQUEST_JWT> in every request with a fresh JWT signing that request's method, path, and body. Example bodies exclude the final newline in the code block; the displayed Content-Length values reflect this. The hello body of hello.txt is a five-byte example showing raw file bytes.
info - Retrieve document information
This required endpoint retrieves metadata for a document or directory. It does not return document contents.
Request
GET /tfo-storage/v1/{path}/info
GET /tfo-storage/v1/contracts/hello.txt/info HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
| Item | Format | Contract |
|---|---|---|
path | Path · string | Relative file or directory path. Use /tfo-storage/v1/info for the root |
| Authentication headers | Required · string | Adapter name and JWT for this GET request. See Common headers |
| Request body | None | Omit Content-Type. Set the JWT body length to 0 and hash to the SHA-256 digest of an empty body |
Response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 229
{"path":"contracts/hello.txt","name":"hello.txt","type":"file","size":5,"readable":true,"writable":true,"locked":false,"locker":null,"createdAt":"2026-09-14T00:00:00Z","modifiedAt":"2026-09-14T00:10:00Z","revision":"revision-17"}
| Item | Format | Contract |
|---|---|---|
| Status | 200 OK | Target entry metadata retrieved successfully |
Content-Type | Required header | application/json |
Content-Length | Required header | Exact response JSON length in bytes, at most 5,242,880 |
| Body | object | One entry object containing the metadata fields directly, without an entry or data wrapper |
Required fields are path, name, type, size, readable, writable, and locked. Optional fields are locker, createdAt, modifiedAt, and revision; they may be omitted or null. Field types, lengths, and allowed values follow the common entry schema. The response path must match the decoded request path. For the root, use path: "", type: "directory", and size: 0.
| Status | Conditions and handling |
|---|---|
404 Not Found | The target does not exist. Office treats the entry as absent and does not require fabricated metadata |
413 Payload Too Large | File size or response JSON exceeds its limit |
501 Not Implemented | Not accepted as an unsupported capability because this endpoint is required |
list - List child entries
This optional endpoint lists the immediate files and directories within a directory. It provides neither recursive listing nor pagination.
Request
GET /tfo-storage/v1/{path}/list
GET /tfo-storage/v1/contracts/list HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
| Item | Format | Contract |
|---|---|---|
path | Path · string | Directory to list. Use /tfo-storage/v1/list for the root |
| Authentication headers | Required · string | Adapter name and JWT for this GET request. See Common headers |
| Request body | None | Omit Content-Type. Set the JWT body length to 0 and hash to the SHA-256 digest of an empty body |
Response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 243
{"entries":[{"path":"contracts/hello.txt","name":"hello.txt","type":"file","size":5,"readable":true,"writable":true,"locked":false,"locker":null,"createdAt":"2026-09-14T00:00:00Z","modifiedAt":"2026-09-14T00:10:00Z","revision":"revision-17"}]}
| Item | Format | Contract |
|---|---|---|
| Status | 200 OK | Directory listed successfully, including an empty directory |
Content-Type | Required header | application/json |
Content-Length | Required header | Exact length of the complete JSON in bytes, at most 5,242,880 |
entries | Body · array · required | 0–10,000 entry objects; the only field in the top-level object |
entries[] | object | Same required and optional fields as info. Each decoded path must be an immediate child of the listed directory, without duplicates |
Entry paths are always relative to the root: return contracts in a root listing and contracts/hello.txt in a listing of contracts. Do not include the directory itself, its parent, or entries inside subdirectories. Ordering is unspecified. Do not send additional fields such as total, cursor, or hasMore.
Return 200 with {"entries":[]} for an empty directory. This body is 14 bytes long. If listing is not implemented, return 501 with LIST_NOT_SUPPORTED after authentication. An empty listing, a missing target (404), and an unsupported operation are distinct results.
| Status | Conditions and handling |
|---|---|
404 Not Found | The directory to list does not exist |
409 Conflict | The target is not a directory (public examples) |
413 Payload Too Large | Entry count, metadata size, or an included file size exceeds its limit. Do not return a partial listing as successful |
501 Not Implemented | After authentication, {"code":"LIST_NOT_SUPPORTED"}. File listing is unavailable in Office |
get - Download a document
This required endpoint returns the complete original file bytes. Office reads this content to open the document.
Request
GET /tfo-storage/v1/{path}/get
GET /tfo-storage/v1/contracts/hello.txt/get HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
| Item | Format | Contract |
|---|---|---|
path | Path · string | File to download. Use list to list a directory |
| Authentication headers | Required · string | Adapter name and JWT for this GET request. See Common headers |
| Request body | None | Omit Content-Type. Set the JWT body length to 0 and hash to the SHA-256 digest of an empty body |
Range requests for partial downloads are not part of this contract. Return the file itself, rather than a download URL, JSON, or Base64.
Response
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 5
hello
| Item | Format | Contract |
|---|---|---|
| Status | 200 OK | Complete file downloaded successfully |
Content-Type | Required header | application/octet-stream, regardless of file extension |
Content-Length | Required header | Actual file length in bytes, 0–314,572,800 |
| Body | binary | Original file bytes of exactly Content-Length; no JSON fields |
Return an empty file with 200 and Content-Length: 0. Do not substitute compression, chunked transfer, redirects, or partial responses. Read a consistent version from storage so that changes during streaming cannot cause a mismatch with the declared length.
| Status | Conditions and handling |
|---|---|
404 Not Found | The file does not exist |
409 Conflict | The target is not a file (public examples) |
413 Payload Too Large | The file exceeds the size limit. Reject it before sending success headers or file bytes |
501 Not Implemented | Not accepted as an unsupported capability because this endpoint is required |
put - Save a document
Receives the complete edited file as raw bytes. Partial updates and multipart uploads are not used. Buffer all bytes in temporary storage, verify authentication, length, hash, and access permissions, then atomically replace the target file. Preserve the existing file on failure.
Request
PUT /tfo-storage/v1/{path}/put
PUT /tfo-storage/v1/contracts/hello.txt/put HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/octet-stream
Content-Length: 5
hello
| Item | Format | Contract |
|---|---|---|
path | Path · string | Relative path of the file to save. An empty path cannot overwrite the Provider root |
| Authentication headers | Required · string | Adapter name and JWT for this PUT request. See Common headers |
Content-Type | Required header | Exactly application/octet-stream |
Content-Length | Required header | Actual complete file length in bytes, 0–314,572,800 |
| Request body | binary | Complete file bytes, not a JSON object, Base64, or multipart data |
request.arguments.save_type | JWT · string · optional | Save context supplied by Office, such as save. Do not send it as a separate body field or query parameter |
save_type does not prove access permissions or lock ownership. This request defines no revision, If-Match, or lock-token fields for conditional saves. The Provider determines whether concurrent saves are accepted or rejected as conflicts. The public examples create or replace a file when its parent directory exists; they do not create parent directories automatically.
Response
After committing the file to storage, return the saved document's identifier as a JSON object.
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 29
{"docId":"saved-document-id"}
| Item | Format | Contract |
|---|---|---|
| Status | 200 OK | Other 2xx statuses must pass body validation. 204 No Content is not allowed |
Content-Type | Required header | UTF-8 application/json |
Content-Length | Required header | Exact JSON body byte count. The fixed-length rules apply |
| Body | JSON object · required | Contains only docId. Extra or duplicate fields, trailing JSON values, and trailing text are not allowed |
docId | string · required | 1–1,024 ASCII letters, digits, ., _, :, or -, starting with a letter or digit. true and false are reserved regardless of case |
docId identifies the saved destination. Within an adapter, the same destination keeps the same ID when its contents change, and different destinations have different IDs. This applies to both regular saves and Save As. Do not substitute a revision or the original session's document ID.
Empty bodies, plain text IDs, XML, JSON strings, numbers, booleans (including true), null, and arrays are rejected. Do not trim IDs or convert other formats automatically. See the PUT response schema for the machine-readable contract. The Node.js, Spring Boot, and FastAPI examples hash the decoded root-relative path as UTF-8 with SHA-256 and return the 64-character lowercase hexadecimal result as the ID; production Providers can use their own document IDs that satisfy the same format.
| Status | Conditions and handling |
|---|---|
400 Bad Request | Empty document path, declared length differing from the actual body, or similar invalid input |
404 Not Found | The parent directory does not exist (public examples) |
409 Conflict | A directory occupies the file path, or the storage conflict policy rejects the save |
413 Payload Too Large | The document exceeds the protocol limit or a lower Provider limit |
501 Not Implemented | After authentication, {"code":"PUT_NOT_SUPPORTED"}. Nothing is written to storage, and Office reports a save failure |
On failure, preserve the existing file and clean up temporary data. Saving to the same destination again returns the same docId, even when contents or revisions change.
lock - Lock a document
This optional endpoint records a document's lock owner. A repeated request from the same owner succeeds; an existing lock held by another owner is a conflict. Implement it together with unlock.
Request
POST /tfo-storage/v1/{path}/lock
POST /tfo-storage/v1/contracts/hello.txt/lock HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 20
{"owner":"editor-1"}
| Item | Format | Contract |
|---|---|---|
path | Path · string | Relative path of the document to lock |
| Authentication headers | Required · string | Adapter name and JWT for this POST request. See Common headers |
Content-Type | Required header | Exactly application/json |
Content-Length | Required header | UTF-8 byte length of the JSON body |
owner | Body · string · required | Nonempty lock owner identifier; the only JSON field |
Store owner unchanged and compare it for equality when processing unlock requests. Do not use it as a user authentication token. A value returned as locker in info or list must fit that field's 255-character limit. This request defines no lock expiration, lease duration, or renewal interval fields.
Response
HTTP/1.1 204 No Content
| Item | Format | Contract |
|---|---|---|
| Status | 204 No Content | A new lock was acquired or an existing lock for the same owner was confirmed. Other 2xx statuses also succeed |
| Body | None | No lock token or result JSON is required. Bodies of other 2xx responses contain no fields required to determine success |
| Status | Conditions and handling |
|---|---|
400 Bad Request | Missing owner, empty string, incorrect type, or extra JSON fields |
404 Not Found | The target to lock does not exist (public examples) |
409 Conflict | Another owner holds the lock. Do not change the existing lock |
501 Not Implemented | After authentication, {"code":"LOCK_NOT_SUPPORTED"}. Office treats this as successful without an actual lock |
Generate a fresh JWT even when locking again with the same owner. If locking is unsupported, unlock must also be unsupported. Never disguise an actual lock conflict as LOCK_NOT_SUPPORTED.
unlock - Unlock a document
This optional endpoint releases the current owner's lock. It succeeds if no lock remains and returns a conflict if another owner's lock would be released. Implement it together with lock.
Request
POST /tfo-storage/v1/{path}/unlock
POST /tfo-storage/v1/contracts/hello.txt/unlock HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 20
{"owner":"editor-1"}
| Item | Format | Contract |
|---|---|---|
path | Path · string | Relative path of the document to unlock |
| Authentication headers | Required · string | Adapter name and JWT for this POST request. See Common headers |
Content-Type | Required header | Exactly application/json |
Content-Length | Required header | UTF-8 byte length of the JSON body |
owner | Body · string · required | The same owner identifier used to acquire the lock; the only JSON field |
There is no force-unlock feature that omits owner or supplies a different value.
Response
HTTP/1.1 204 No Content
| Item | Format | Contract |
|---|---|---|
| Status | 204 No Content | The same owner's lock was released, or no lock remains. Other 2xx statuses also succeed |
| Body | None | No unlock result JSON is required |
| Status | Conditions and handling |
|---|---|
400 Bad Request | Missing owner, empty string, incorrect type, or extra JSON fields |
409 Conflict | The current lock owner differs. Preserve the existing lock |
501 Not Implemented | After authentication, {"code":"UNLOCK_NOT_SUPPORTED"}. Office treats this as successful without an actual unlock operation |
Use a fresh JWT even when retrying an already released lock. If unlocking is unsupported, lock must also be unsupported.
mkdir - Create a directory
This optional endpoint creates one directory directly under the specified parent. It does not create a multilevel path with intermediate directories.
Request
POST /tfo-storage/v1/{parent}/mkdir
POST /tfo-storage/v1/contracts/mkdir HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 18
{"name":"archive"}
| Item | Format | Contract |
|---|---|---|
parent | Path · string | Relative path of an existing parent directory. Use /tfo-storage/v1/mkdir to create a directory under the root |
| Authentication headers | Required · string | Adapter name and JWT for this POST request. See Common headers |
Content-Type | Required header | Exactly application/json |
Content-Length | Required header | UTF-8 byte length of the JSON body |
name | Body · string · required | One new directory name, 1–255 characters; the only JSON field |
name is the actual name, not a URL-encoded path. /, \, control characters, and names equal to . or .. are not allowed. For example, 2026/reports cannot create a nested path in one request.
Response
HTTP/1.1 204 No Content
| Item | Format | Contract |
|---|---|---|
| Status | 204 No Content | The parent/name directory was created. Other 2xx statuses also succeed |
| Body | None | No created entry object is required. Retrieve it later with info or list |
| Status | Conditions and handling |
|---|---|
400 Bad Request | Missing name, incorrect type, extra fields, or an invalid name |
404 Not Found | The parent directory does not exist (public examples) |
409 Conflict | An entry with the same name exists, or the parent is not a directory (public examples) |
501 Not Implemented | After authentication, {"code":"MKDIR_NOT_SUPPORTED"}. No directory is created |
The public examples return 409 even when repeating a creation request for an already created directory. If a response is lost, check whether the entry was created before retrying.
rename - Rename an entry
This optional endpoint renames a file or directory within the same parent directory. It defines no options for moving entries to another directory or overwriting an existing entry.
Request
POST /tfo-storage/v1/{path}/rename
POST /tfo-storage/v1/contracts/hello.txt/rename HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 23
{"name":"greeting.txt"}
| Item | Format | Contract |
|---|---|---|
path | Path · string | Relative path of the existing entry to rename. The Provider root cannot be renamed |
| Authentication headers | Required · string | Adapter name and JWT for this POST request. See Common headers |
Content-Type | Required header | Exactly application/json |
Content-Length | Required header | UTF-8 byte length of the JSON body |
name | Body · string · required | One new name, 1–255 characters; the only JSON field |
Use the actual decoded name for name, following the mkdir name rules. Sending greeting.txt for contracts/hello.txt results in contracts/greeting.txt.
Response
HTTP/1.1 204 No Content
| Item | Format | Contract |
|---|---|---|
| Status | 204 No Content | The entry was renamed within the same parent. Other 2xx statuses also succeed |
| Body | None | No new path or entry object is required. Use info on the new path or list on its parent to retrieve it |
| Status | Conditions and handling |
|---|---|
400 Bad Request | Invalid name, extra JSON fields, or an attempt to rename the Provider root |
404 Not Found | No entry exists at the original path |
409 Conflict | The new name is already in use. The public examples also reject renaming locked entries |
501 Not Implemented | After authentication, {"code":"RENAME_NOT_SUPPORTED"}. The original name is preserved |
After a successful rename, a retry at the original path may find no target. If the response is lost, check both the original and new paths.
delete - Delete an entry
This optional endpoint deletes a file or directory. Reject attempts to delete the Provider root. The Provider must explicitly define whether it rejects nonempty directories or deletes them recursively; the public examples reject them with 409.
Request
DELETE /tfo-storage/v1/{path}/delete
DELETE /tfo-storage/v1/contracts/greeting.txt/delete HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
| Item | Format | Contract |
|---|---|---|
path | Path · string | Relative path of the entry to delete. The Provider root cannot be deleted |
| Authentication headers | Required · string | Adapter name and JWT for this DELETE request. See Common headers |
| Request body | None | Omit Content-Type. Set the JWT body length to 0 and hash to the SHA-256 digest of an empty body |
There are no query or body options such as recursive or force.
Response
HTTP/1.1 204 No Content
| Item | Format | Contract |
|---|---|---|
| Status | 204 No Content | The entry was deleted. Other 2xx statuses also succeed |
| Body | None | No deleted entry object or deletion count is required |
| Status | Conditions and handling |
|---|---|
400 Bad Request | Attempt to delete the Provider root |
404 Not Found | The entry to delete does not exist, including a retry for an already deleted entry (public examples) |
409 Conflict | Deletion of a nonempty directory or locked entry was rejected (public examples) |
501 Not Implemented | After authentication, {"code":"DELETE_NOT_SUPPORTED"}. The entry is not deleted |
Unsupported operations
Use the following response only for an optional operation that is not implemented. Return it after authentication, body verification, and replay checks for that request. The status must be 501, the media type must be application/json, and the JSON body must contain only one code field matching the current operation. A charset parameter is allowed.
| Item | Format | Contract |
|---|---|---|
| Status | Required | Exactly 501 Not Implemented |
Content-Type | Required header | application/json; a charset parameter is allowed |
code | Body · string · required | The exact value for the current operation from the table below. No additional fields |
| Body length | Brief UTF-8 JSON | The complete object must fit within Office's 8 KiB error-body read limit |
The fixed-length checks for successful read responses do not apply here, but you may include an exact Content-Length as in the example below.
HTTP/1.1 501 Not Implemented
Content-Type: application/json
Content-Length: 29
{"code":"LIST_NOT_SUPPORTED"}
| Operation | Exact code | Office behavior |
|---|---|---|
list | LIST_NOT_SUPPORTED | File listing is unavailable. The connection can be saved after acknowledging the notice during registration |
put | PUT_NOT_SUPPORTED | Saving fails |
lock | LOCK_NOT_SUPPORTED | Treated as successful without an actual lock |
unlock | UNLOCK_NOT_SUPPORTED | Treated as successful without an actual unlock |
mkdir | MKDIR_NOT_SUPPORTED | Directory creation is unavailable |
rename | RENAME_NOT_SUPPORTED | Renaming is unavailable |
delete | DELETE_NOT_SUPPORTED | Deletion is unavailable |
Because info and get are required, every 501 fails those operations. For other operations, an empty body, HTML, additional JSON fields, another operation's code, 404, or another 5xx status is not an unsupported declaration. For example, {"code":"LIST_NOT_SUPPORTED","message":"..."} or DELETE_NOT_SUPPORTED returned for list is invalid.
The only unsupported locking responses Office treats as successful are the exact LOCK_NOT_SUPPORTED and UNLOCK_NOT_SUPPORTED pair. Do not declare only one unsupported. Because no actual lock exists, concurrent saves follow the storage service's last-write or conflict policy. A 409 from implemented locking, authentication failures, network errors, and storage failures remain actual failures.
The admin connection test calls only list. Verify other capabilities through actual document operations; registration does not arbitrarily execute writes or deletes. Also see the unsupported operation JSON schema.
Error responses
Office treats 404 from info as an absent target. Except for that case and the exact unsupported responses above, non-2xx responses fail the Office operation. Ordinary errors have no fixed JSON body schema. Return a brief error description without secrets, JWTs, internal paths, document contents, or stack traces. Office may display up to 8 KiB of the error body.
| Item | Format | Contract |
|---|---|---|
| Status | HTTP status code | Choose according to the error cause below. The status determines success or failure |
Content-Type | Match the body | No fixed media type for ordinary errors. The public examples use text/plain; charset=utf-8 |
| Body | text or Provider-defined JSON | Optional. Description strings in the public examples are not fixed protocol error identifiers |
The following is an authentication error response. Do not distinguish unknown adapters, signature mismatches, expiration, and replay through detailed response messages.
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Content-Length: 12
Unauthorized
| Status | When to use |
|---|---|
400 Bad Request | Invalid path, JSON, body length, or a disallowed query |
401 Unauthorized | Unknown adapter, invalid signature or claims, or an expired or reused JWT |
403 Forbidden | No permission to access the storage or document |
404 Not Found | Target entry or request path does not exist |
405 Method Not Allowed | Incorrect method for a known operation path (public examples) |
409 Conflict | Name, type, or lock-owner conflict, or rejection of nonempty directory deletion |
411 Length Required | Required body Content-Length is missing |
413 Payload Too Large | Request body, document, or listing exceeds its limit, or a Provider-defined limit is exceeded |
415 Unsupported Media Type | Request Content-Type does not match the operation contract |
500 Internal Server Error | Unhandled internal Provider error (public examples). Do not expose exception details in the response |
501 Not Implemented | Exact unsupported declaration for an optional operation as specified above |
503 Service Unavailable | Temporary storage failure |
Do not turn authentication errors into unsupported responses or report storage failures as successful. Preserve the existing file and clean up temporary files when put fails. Do not retry oversized requests or responses using chunked transfer.
The public examples separately limit JSON request bodies to 16 KiB. This is a different limit from the 5 MiB response limit for info and list. See Example configuration and setup for deployment-specific limits.