Skip to main content

HTTP Storage Protocol

HTTP Storage Protocol v1 defines requests and responses between the Office HTTP Storage adapter and a customer-operated HTTP Storage Provider. You implement the Provider endpoints below; 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.

From document listing to closing​

First, prepare code that builds a document-open URL in your application. This example assumes that the http-remote-directory adapter is connected to a Provider that implements list, put, lock, and unlock.

  1. Choose the Office address and registered adapter name. Send /open to the Office address, not the Provider.
  2. Put the selected document's relative path in the URL path and set app, docId, and user_id.
  3. Add a userSessionToken issued by your service after checking the user and document access. Serialize the metadata as a JSON string and set __tfo_adapter_http_storage_client_metadata; URLSearchParams applies URL encoding once.
function createOpenUrl(documentPath, docId, userSessionToken) {
const encodedPath = documentPath.split("/").map(encodeURIComponent).join("/");
const url = new URL(
`/cloud-office/api/http-remote-directory/${encodedPath}/open`,
"https://office.example.com",
);
url.searchParams.set("app", "WORD_EDITOR");
url.searchParams.set("docId", docId);
url.searchParams.set("user_id", "example-user");
url.searchParams.set("__tfo_adapter_http_storage_client_metadata", JSON.stringify({
project: "example-project",
userSessionToken,
}));
return url.toString();
}

const openUrl = createOpenUrl(
"contracts/sample.docx",
"sample01",
"example-short-lived-document-session-token",
);

The token above is illustrative. Use the value issued by your service when the document is selected. Open the completed openUrl in the browser or set it as the Office iframe's src. The customer metadata example shows the JSON string, encoded parameter, and complete URL.

After preparing this URL builder, the sequence below follows listing → document selection and URL creation/opening → editing → saving → closing. list serves the document selection screen; info and later operations handle the selected document's /open and subsequent work. Listing also requires the customer session and listing permission expected by the Provider.

TFO and HTTP Storage Adapter are built into the Office framework; you do not need to implement them. For the storage integration, the component you must implement and operate is HTTP Storage Provider. Your application builds the document-open URL and supplies the customer session as shown above.

The Office Adapter signs a JWT containing the document path, request details, and request.client_metadata, then places it in the X-TFO-Storage-Request-JWT header of each HTTP request to the Provider. Document file bytes travel in the HTTP body rather than the JWT.

On every HTTP request, the Provider must verify the JWT signature and compare the signed request details with the actual request. You must also validate the customer session and the current permissions for the document and operation. Reject failed verification or authorization before accessing storage. Follow JWT signing and verification for the complete checks.

Document workflow with built-in Office components, JWT headers, and customer Provider responsibilitiesThe built-in Office HTTP Storage Adapter sends signed requests to the customer Provider. The Provider checks the request and customer document permission before reading or saving a document.List, open, edit, save, and closeOffice framework · built in (no customer implementation)Customer implements and operatesTFOHTTP Storage AdapterHTTP Storage ProviderAdapter → Provider: example info HTTP request · a fresh JWT per requestGET /tfo-http-storage/v1/contracts/sample.docx/info HTTP/1.1Host: storage.example.comX-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>Inside the JWT: request.path · request.client_metadata { project, userSessionToken }1. Browse the document selection listlist()GET …/list + JWT200 · entriesDisplay document list2. Select a document · the customer app builds and opens its /open URLCustomer metadata in /open → JWT request.client_metadatainfo()GET …/info + JWT200 · file metadataDocument info verifiedMissing document, invalid authentication, or denied access → stop openingThe flow below continues after successful verification.lock()POST …/lock + JWT · owner204 · lockedDocument lock confirmedget()GET …/get + JWT200 · document bytesDisplay document in editor3. Edit the document · select Saveput()PUT …/put + JWT · file bytes200 · { docId }Save confirmed4. Last editor closes after savingunlock()POST …/unlock + JWT · owner204 · unlockedUnlock confirmedThe Provider checks JWT signatures, validity periods, and replay on every request.Compare signed and received request details; check the session and document/operation access.Reject failed verification or authorization before accessing storage.Closing sends no separate close or stop request to the Provider.Complete each request independently of earlier request-processing state (stateless).

Implement each endpoint statelessly: complete the request without depending on intermediate processing state from an earlier request. For example, get must not rely on temporary state created by an earlier info call. It must be able to verify the incoming request and read the document independently.

The lock and unlock operations must share the document's lock state and owner. You must also retain JWT replay-protection records. These are separate from carrying intermediate request state from info to get.

Closing a document sends no separate close or stop request to the Provider. An automatic save during closing uses put. See Save types for the meaning of save_type: close.

Request order and frequency can vary with editor state and collaboration. A document may be saved several times while editing. The diagram shows the last participant finishing a save and closing the document. See Unsupported operations for Providers that do not implement lock and unlock.

URLs and paths​

{PROVIDER_BASE_URL}/tfo-http-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-http-storage/v1/list. Do not insert empty intermediate segments or extra / characters.

ItemExample
Provider base URLhttps://storage.example.com/office
Document pathcontracts/Proposal 2026.docx
Document information requestGET /office/tfo-http-storage/v1/contracts/Proposal%202026.docx/info
Root listing requestGET /office/tfo-http-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​

HeaderScopeValue
X-TFO-Storage-Request-JWTRequired on every requestThe raw JWT signed according to the rules below, without a Bearer prefix
Content-Typeput, lock, unlock, mkdir, renameapplication/octet-stream for put; application/json for JSON operations
Content-LengthRequired on requests with a bodyA 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. Send JSON as UTF-8 bytes and include their length and SHA-256 digest in the signed JWT.

JWT signing and verification​

Office signs a JWT containing request details with the shared secret. Before reading or modifying a document, the Provider must verify the signature with that secret and compare the signed method, path, and body details with the received request. This detects changes in transit. It must also check the JWT's validity period and unique request ID to reject expired or reused JWTs.

Signing does not encrypt document contents. Use HTTPS to protect data in transit, and separately check user authentication and document permissions through your service's session and permission records.

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, signed with HS256, and the following header.

{
"alg": "HS256",
"typ": "tfo-storage-request+jwt"
}

The following payload represents a request to save contracts/sample.docx. The file size and SHA-256 digest are illustrative; calculate them from the file you send. Generate fresh iat, exp, and jti values for every request.

{
"iss": "thinkfree-office",
"aud": "tfo-http-storage-provider",
"iat": 1789344000,
"exp": 1789344060,
"jti": "26bc9b5e-9dab-4a78-95d8-b1ae34a5d9eb",
"request": {
"adapter": "http-remote-directory",
"method": "PUT",
"path": "/tfo-http-storage/v1/contracts/sample.docx/put",
"content_type": "application/octet-stream",
"content_length": 231958,
"content_sha256": "754464a613ba4d87d1535f69a9b1e3a0a8f84b8277d2bf4d0c74aacceceb23d7",
"office_connection_id": "example-connection",
"arguments": {
"save_type": "save"
},
"client_metadata": {
"project": "example-project",
"userSessionToken": "example-short-lived-document-session-token"
}
}
}
FieldTypeRequiredValidation rule
Header algstringYesExactly HS256
Header typstringYesExactly tfo-storage-request+jwt
issstringYesExactly thinkfree-office
audstring or single-element arrayYestfo-http-storage-provider, or an array containing only that value
iatintegerYesIssued-at Unix time in seconds, not later than the current time
expintegerYesExpiration Unix time in seconds, later than the current time, with 0 < exp - iat <= 60
jtistringYesA unique ID for each request
requestobjectYesContains the signed request fields below
request.adapterstringYesRegistered adapter name: 1–128 ASCII alphanumeric, ., _, or - characters, beginning with an alphanumeric character. Must match the adapter name associated with the secret used to verify the signature
request.methodstringYesMatches the actual uppercase HTTP method
request.pathstringYesMatches the actual raw encoded path, including the Provider base path, without scheme, host, or query
request.content_typestringWhen the header is presentExactly matches the actual Content-Type
request.content_lengthintegerYesThe actual number of body bytes received, or 0 when there is no body
request.content_sha256stringYesSHA-256 of the actual body bytes, as 64 lowercase hexadecimal characters
request.office_connection_idstringNoIdentifies an Office connection; do not interpret its internal structure or use it as proof of user authentication
request.argumentsobjectNoOperation context such as save_type; does not grant access
request.client_metadataobjectNoCustomer-defined JSON object that the Provider can use for user authentication and authorization after signature verification

The complete JWT must not exceed 8,192 bytes. See Client metadata for the client_metadata input and limits.

Verification order​

  1. Check that the request method, path, headers, and body size meet the requirements.
  2. Use request.adapter to select a configured secret, then verify the original JWT's signature, issuer, audience, and validity period. Use the unverified adapter name only to select the secret.
  3. Compare the signed request fields with the actual request. Calculate the body length and SHA-256 digest from the received bytes.
  4. Check and record jti in one atomic operation, and reject reuse of that JWT until exp. Multiple Provider instances must share these records.
  5. Check document access permissions, then perform the operation or return an unsupported operation response.

Reject requests that fail verification before accessing storage. Keep the Office and Provider server clocks synchronized. Office must generate fresh iat, exp, and jti values for retries. jti is not an idempotency key that prevents duplicate operations, so check storage state if a response is lost.

Request authentication followed by customer document authorizationThe built-in Office HTTP Storage Adapter sends signed requests to the customer Provider. The Provider checks the request and customer document permission before reading or saving a document.Provider request processing1. Use unverified request.adapter to select a registered keyThis step grants no document access2. Verify the JWT and compare it with the received requestSignature, issuer, audience, validity period, request details and replay3. Check the customer session and document accessUse the client_metadata token to verify the user and operation permissions4. Perform the authorized storage operationReturn the result of info / get / put or another operationFailure / denialError responseNo document accessRequest authentication and customer authorization are separate. Both must pass.

Client metadata​

To pass customer-defined context when opening a document, supply __tfo_adapter_http_storage_client_metadata as a JSON string in the Office /open parameters. URL-encode it once when using a query string. For a signed document-open ticket, put the same JSON string in command.params under that name; unsigned query parameters cannot override the signed value. This is not a Provider endpoint query parameter.

The request JWT received by the Provider contains the parsed JSON object in request.client_metadata.

For example, build the following URL to open contracts/sample.docx through the http-remote-directory adapter. Replace the Office address and document path for your environment, and use a real token issued by your service. The token below is illustrative.

const metadata = {
project: "example-project",
userSessionToken: "example-short-lived-document-session-token",
};
const openUrl = new URL(
"/cloud-office/api/http-remote-directory/contracts/sample.docx/open",
"https://office.example.com",
);
openUrl.searchParams.set("app", "WORD_EDITOR");
openUrl.searchParams.set("docId", "sample01");
openUrl.searchParams.set("user_id", "example-user");
openUrl.searchParams.set(
"__tfo_adapter_http_storage_client_metadata",
JSON.stringify(metadata),
);
console.log(openUrl.toString());

The customer application produces this JSON string with JSON.stringify(metadata):

{"project":"example-project","userSessionToken":"example-short-lived-document-session-token"}

URLSearchParams encodes it once and adds the following parameter to /open. Do not encode this value again.

__tfo_adapter_http_storage_client_metadata=%7B%22project%22%3A%22example-project%22%2C%22userSessionToken%22%3A%22example-short-lived-document-session-token%22%7D

The complete document-open URL is shown below. The Provider receives the request.client_metadata object inside the JWT, rather than this URL's query string.

https://office.example.com/cloud-office/api/http-remote-directory/contracts/sample.docx/open?app=WORD_EDITOR&docId=sample01&user_id=example-user&__tfo_adapter_http_storage_client_metadata=%7B%22project%22%3A%22example-project%22%2C%22userSessionToken%22%3A%22example-short-lived-document-session-token%22%7D
ItemRequiredLimit
Metadata inputOptional; a Provider's access policy may require itJSON object encoded as a string; the 4,096 UTF-8 byte limit applies only to the original input
Object keyFor each field1–64 UTF-16 code units; not blank according to Java String.isBlank()
String valueWhen presentAt most 512 UTF-16 code units
NestingYesRoot object depth 0; each object field value or array element adds 1; maximum 8

The Provider must also check received metadata against the structural limits above.

You can pass a token issued by your service for document access, such as userSessionToken above, so the Provider can authenticate the user. After verifying the JWT, the Provider checks this token against your service's session records to verify the user, expiration, target document, and allowed operations. userSessionToken is an example field name; your service implements token issuance and verification.

Check permissions on every request: info and get require read permission, and put requires write permission. Reject requests that require this token if it is missing, expired, revoked, or lacks permission for the operation.

Metadata is not encrypted: it can appear in the open URL, connection state, and request JWT. Do not include login cookies, refresh tokens, shared signing secrets, long-lived credentials, or document contents. If metadata carries an access capability, issue a separate short-lived value scoped to the document and allowed operations, and redact it from logs.

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.

TargetContent-TypeLimit
Successful info, list, and put responsesapplication/json5 MiB each (5,242,880 bytes)
Successful get responseapplication/octet-stream300 MiB (314,572,800 bytes)
put requestapplication/octet-stream300 MiB (314,572,800 bytes)
File size in metadataJSON integer0–314,572,800
entries in listJSON arrayAt most 10,000 entries, with the complete JSON also limited to 5 MiB

The limits in the table are inclusive. A Provider may impose a lower document size limit.

Office fails the operation if headers, size, or actual body length do not meet these requirements. Unsupported operation responses and ordinary errors follow their respective sections below.

Success statuses and bodies​

OperationNormal responseBody format
info, list200 OKA JSON object is required; do not substitute an empty body or 204
get200 OKThe complete file bytes; return an empty file with Content-Length: 0
put200 OKA JSON object with docId is required. See PUT response
lock, unlock, mkdir, rename, delete204 No ContentNo body

Office also accepts other 2xx statuses, but info, list, get, and put must pass response body validation. The remaining operations require no result fields, so use 204 without a body. If result text is needed, return 200 with text/plain; charset=utf-8, up to 10 MiB.

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.

FieldTypeRequiredMeaning and limits
pathstringYesDecoded path relative to the root, at most 4,096 characters. The root uses an empty string
namestringYesA 1–255 character name matching the final path segment. The root uses a fixed display name
typestringYesfile or directory
sizeintegerYesFile length in bytes, 0–314,572,800. Must be 0 for directories
readablebooleanYesWhether the document can be read
writablebooleanYesWhether the document can be written
lockedbooleanYesCurrent lock state
lockerstring or nullNoLock owner, at most 255 characters
createdAtstring or nullNoRFC 3339 creation time, at most 64 characters
modifiedAtstring or nullNoRFC 3339 modification time, at most 64 characters
revisionstring or nullNoStorage revision or ETag-equivalent value, at most 1,024 characters

info.path must match the requested document path. list contains only the requested directory's immediate children, with no duplicate paths. Pagination is not supported.

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; the Provider must check permissions again on each request. revision describes the storage version and is not automatically used as a conditional request header or lock token in this protocol.

To validate the JSON response format, use the entry schema, info response schema, and list response schema. Schema validation alone cannot verify every relationship between the request and response paths. Also check the path conditions and size limits above.

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 X-TFO-Storage-Request-JWT header described in Common request headers. Query parameters are not supported.

The request bodies for lock, unlock, mkdir, and rename are JSON objects containing only the field specified for that operation. 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.

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.

OperationMethodPath suffixImplementation
infoGET/{path}/infoRequired
listGET/{path}/listOptional
getGET/{path}/getRequired
putPUT/{path}/putOptional
lockPOST/{path}/lockImplement with unlock, or declare both unsupported
unlockPOST/{path}/unlockImplement with lock, or declare both unsupported
mkdirPOST/{parent}/mkdirOptional
renamePOST/{path}/renameOptional
deleteDELETE/{path}/deleteOptional

The examples below use a Provider at https://storage.example.com to read and save contracts/sample.docx, rename it to reviewed.docx, and then delete it. 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. In the get response and put request, <DOCX binary> represents the omitted DOCX file bytes. Send the raw file bytes in place of this text and replace <DOCX_BYTE_LENGTH> with their actual byte count.

Reading the implementation snippets​

The Node.js (Express), Java (Spring MVC), and Python (FastAPI) snippets show request parsing, JWT verification, storage calls, and responses for each operation. Selecting a language changes all snippet tabs together.

These snippets use the helpers and configuration from the public Provider examples; they are not standalone programs. Each language tab explains where to place the code and which imports and configuration it needs.

Implement storage access and customer-session and document-permission checks for your service. Use the original URL-encoded request path for JWT verification.

Shared helpers and storage service used by this snippet: Node.js Provider source

Start from the public example's storage-router.mjs. Keep its imports and shared helpers. Inside createStorageRouter, replace the protocol-handling router.use with the operation handlers below. Keep the existing error-handling middleware.

requestVerifier.verify(request, { rawPath }, body) verifies the JWT signature and validity period, then compares the signed request details with the actual request. It needs body to check the received body's length and SHA-256. Verification errors are handled by the middleware, which returns 401 Unauthorized.

Required imports in the existing example (keep them if already present)

import { rm } from "node:fs/promises";
import { requirePathSegment } from "../domain/storage-route.mjs";

If JWT verification fails, return an error before accessing storage. If you use customer sessions, add user authentication and document/operation permission checks where indicated by the comments.

info - Retrieve document information​

This required endpoint retrieves metadata for a document or directory. It does not return document contents.

Request​

GET /tfo-http-storage/v1/{path}/info

GET /tfo-http-storage/v1/contracts/sample.docx/info HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
ItemFormatDescription
pathPath · stringRelative file or directory path. Use /tfo-http-storage/v1/info for the root
Authentication headersRequired · stringJWT for this GET request. See Common headers
Request bodyNoneOmit Content-Type. Follow the common request rules

Implementation example​

Shared helpers and storage service used by this snippet: Node.js Provider source

// GET /tfo-http-storage/v1/{path}/info
router.get(/^\/tfo-http-storage\/v1\/(?:.+\/)?info$/, async function infoDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// Strip the prefix and operation from the raw path; decode each directory/file segment.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/info".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// Require no body. Use length 0 and the SHA-256 of the empty body.
const body = await readRequestBody(request, "none", config, stateStore);
// Verify the JWT signature and validity period, match the request details, and check for replay.
// Read X-TFO-Storage-Request-JWT from request.headers.
// Even without a body, length 0 and the empty-body SHA-256 must match the JWT.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// For customer sessions, validate the token in client_metadata and check document/operation permissions here.
sendJson(response, await storageService.info(segments));
});

Response​

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 283

{
"path": "contracts/sample.docx",
"name": "sample.docx",
"type": "file",
"size": 231958,
"readable": true,
"writable": true,
"locked": false,
"locker": null,
"createdAt": "2026-09-14T00:00:00Z",
"modifiedAt": "2026-09-14T00:10:00Z",
"revision": "revision-17"
}
ItemFormatDescription
Status200 OKTarget entry metadata retrieved successfully
Content-TypeRequired headerapplication/json
Content-LengthRequired headerResponse JSON length in bytes, subject to the size limit
BodyobjectOne entry object containing the metadata fields directly, without an entry or data wrapper

Follow the metadata field requirements. For the root, use path: "", type: "directory", and size: 0.

StatusConditions and handling
404 Not FoundThe target does not exist. Office treats the entry as absent and does not require fabricated metadata
413 Payload Too LargeFile size or response JSON exceeds its limit
501 Not ImplementedNot 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-http-storage/v1/{path}/list

GET /tfo-http-storage/v1/contracts/list HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
ItemFormatDescription
pathPath · stringDirectory to list. Use /tfo-http-storage/v1/list for the root
Authentication headersRequired · stringJWT for this GET request. See Common headers
Request bodyNoneOmit Content-Type. Follow the common request rules

Implementation example​

Shared helpers and storage service used by this snippet: Node.js Provider source

// GET /tfo-http-storage/v1/{path}/list
router.get(/^\/tfo-http-storage\/v1\/(?:.+\/)?list$/, async function listDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// Strip the prefix and operation from the raw path; decode each directory/file segment.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/list".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// Require no body. Use length 0 and the SHA-256 of the empty body.
const body = await readRequestBody(request, "none", config, stateStore);
// Verify the JWT signature and validity period, match the request details, and check for replay.
// Read X-TFO-Storage-Request-JWT from request.headers.
// Even without a body, length 0 and the empty-body SHA-256 must match the JWT.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// For customer sessions, validate the token in client_metadata and check document/operation permissions here.
if (config.unsupportedOperations?.has("list")) {
return sendOperationNotSupported(response, "list");
}
sendJson(response, await storageService.list(segments));
});

Response​

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 358

{
"entries": [
{
"path": "contracts/sample.docx",
"name": "sample.docx",
"type": "file",
"size": 231958,
"readable": true,
"writable": true,
"locked": false,
"locker": null,
"createdAt": "2026-09-14T00:00:00Z",
"modifiedAt": "2026-09-14T00:10:00Z",
"revision": "revision-17"
}
]
}
ItemFormatDescription
Status200 OKDirectory listed successfully, including an empty directory
Content-TypeRequired headerapplication/json
Content-LengthRequired headerComplete JSON length in bytes, subject to the size limit
entriesBody · array · required0–10,000 entry objects; the only field in the top-level object
entries[]objectSame 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/sample.docx 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.

For an empty directory, return 200 with an empty entries array. Return 404 if the directory does not exist, or 501 with LIST_NOT_SUPPORTED after authentication if listing is unsupported.

StatusConditions and handling
404 Not FoundThe directory to list does not exist
413 Payload Too LargeEntry count, metadata size, or an included file size exceeds its limit. Do not return a partial listing as successful
501 Not ImplementedAfter authentication, 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-http-storage/v1/{path}/get

GET /tfo-http-storage/v1/contracts/sample.docx/get HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
ItemFormatDescription
pathPath · stringFile to download. Use list to list a directory
Authentication headersRequired · stringJWT for this GET request. See Common headers
Request bodyNoneOmit Content-Type. Follow the common request rules

Range requests for partial downloads are not supported. Return the file itself, rather than a download URL, JSON, or Base64.

Implementation example​

Shared helpers and storage service used by this snippet: Node.js Provider source

// GET /tfo-http-storage/v1/{path}/get
router.get(/^\/tfo-http-storage\/v1\/(?:.+\/)?get$/, async function getDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// Strip the prefix and operation from the raw path; decode each directory/file segment.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/get".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// Require no body. Use length 0 and the SHA-256 of the empty body.
const body = await readRequestBody(request, "none", config, stateStore);
// Verify the JWT signature and validity period, match the request details, and check for replay.
// Read X-TFO-Storage-Request-JWT from request.headers.
// Even without a body, length 0 and the empty-body SHA-256 must match the JWT.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// For customer sessions, validate the token in client_metadata and check document/operation permissions here.
const file = await storageService.download(segments);
response.status(200).set({
"Content-Type": "application/octet-stream",
"Content-Length": file.contentLength,
"Cache-Control": "no-store",
});
await pipeline(file.stream, response);
});

Response​

HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: <DOCX_BYTE_LENGTH>

<DOCX binary>
ItemFormatDescription
Status200 OKComplete file downloaded successfully
Content-TypeRequired headerapplication/octet-stream, regardless of file extension
Content-LengthRequired headerActual file length in bytes, 0–314,572,800
BodybinaryOriginal file bytes of exactly Content-Length; no JSON fields

Return an empty file with 200 and Content-Length: 0. Return the same version of the file throughout the download.

StatusConditions and handling
404 Not FoundThe file does not exist
413 Payload Too LargeThe file exceeds the size limit. Reject it before sending success headers or file bytes
501 Not ImplementedNot accepted as an unsupported capability because this endpoint is required

put - Save a document​

Receives the complete edited file in the HTTP body and saves it. Partial updates and multipart uploads are not supported. Verify the request before saving, and preserve the existing file if verification or saving fails.

Request​

PUT /tfo-http-storage/v1/{path}/put

PUT /tfo-http-storage/v1/contracts/sample.docx/put HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/octet-stream
Content-Length: <DOCX_BYTE_LENGTH>

<DOCX binary>
ItemFormatDescription
pathPath · stringRelative path of the file to save. An empty path cannot overwrite the Provider root
Authentication headersRequired · stringJWT for this PUT request. See Common headers
Content-TypeRequired headerExactly application/octet-stream
Content-LengthRequired headerActual complete file length in bytes, 0–314,572,800
Request bodybinaryComplete file bytes, not a JSON object, Base64, or multipart data
request.arguments.save_typeJWT · string · optionalWhy Office is saving the file. See Save types. Included in the JWT, not in the body or query parameters

Save types​

request.arguments.save_type indicates what triggered the save. Office sends the following values; spelling and capitalization matter.

ValueMeaning
saveA regular save, such as when the user selects Save
saveAsSave As, writing the document to the destination in this request's path
autoSaveAutomatic saving during editing
autoputServer-triggered saving after an editing connection expires, for example after the browser disconnects
closeAn automatic save triggered while closing the document

This field is optional. Process the save even when it is absent; absence does not imply a particular save type.

When closing a document triggers an automatic save, Office calls put with request.arguments.save_type set to close. This value describes why the save occurred; it is not a document-close event. The Provider must save the file. It must not call dispose, release connection or session resources, or assume that no further requests will arrive just because the save type is close.

Implementation example​

Shared helpers and storage service used by this snippet: Node.js Provider source

// PUT /tfo-http-storage/v1/{path}/put
router.put(/^\/tfo-http-storage\/v1\/(?:.+\/)?put$/, async function putDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// Strip the prefix and operation from the raw path; decode each directory/file segment.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/put".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// Enforce the size limit while staging the upload and calculating length and SHA-256.
const body = await readRequestBody(request, "document", config, stateStore);
try {
// Verify the JWT signature and validity period, match the request details, and check for replay.
// Read X-TFO-Storage-Request-JWT from request.headers.
// Compare the received length/SHA-256 in body with the signed JWT values to detect body tampering.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// For customer sessions, validate the token in client_metadata and check document/operation permissions here.
if (config.unsupportedOperations?.has("put")) {
return sendOperationNotSupported(response, "put");
}
const docId = await storageService.save(segments, body.stagingFile);
sendJson(response, { docId });
} finally {
// Delete temporary files left after verification failure, an unsupported response, or a failed save; skip files already moved.
await rm(body.stagingFile, { force: true });
}
});

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: 34

{
"docId": "saved-document-id"
}
ItemFormatDescription
Status200 OKOther 2xx statuses must pass body validation. 204 No Content is not allowed
Content-TypeRequired headerUTF-8 application/json
Content-LengthRequired headerExact JSON body byte count. The fixed-length rules apply
BodyJSON object · requiredContains only docId. Extra or duplicate fields, trailing JSON values, and trailing text are not allowed
docIdstring · required1–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.

Providers can use their own document IDs that satisfy the format above. See the PUT response schema for response validation.

StatusConditions and handling
400 Bad RequestEmpty document path, declared length differing from the actual body, or similar invalid input
409 ConflictThe save conflicts with the destination's current state
413 Payload Too LargeThe document exceeds the protocol limit or a lower Provider limit
501 Not ImplementedAfter authentication, PUT_NOT_SUPPORTED. Nothing is written to storage, and Office reports a save failure

lock - Lock a document​

lock sets a document lock and records its owner. A repeated request with the same owner succeeds; a lock held by another owner returns 409 Conflict. Implement both lock and unlock, or declare both unsupported.

Request​

POST /tfo-http-storage/v1/{path}/lock

POST /tfo-http-storage/v1/contracts/sample.docx/lock HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 25

{
"owner": "editor-1"
}
ItemFormatDescription
pathPath · stringRelative path of the document to lock
Authentication headersRequired · stringJWT for this POST request. See Common headers
Content-TypeRequired headerExactly application/json
Content-LengthRequired headerUTF-8 byte length of the JSON body
ownerBody · string · requiredNonempty 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.

Implementation example​

Shared helpers and storage service used by this snippet: Node.js Provider source

// POST /tfo-http-storage/v1/{path}/lock
router.post(/^\/tfo-http-storage\/v1\/(?:.+\/)?lock$/, async function lockDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// Strip the prefix and operation from the raw path; decode each directory/file segment.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/lock".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// Read the original JSON bytes and calculate their length and SHA-256.
const body = await readRequestBody(request, "json", config, stateStore);
// Verify the JWT signature and validity period, match the request details, and check for replay.
// Read X-TFO-Storage-Request-JWT from request.headers.
// Compare the received length/SHA-256 in body with the signed JWT values to detect body tampering.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// For customer sessions, validate the token in client_metadata and check document/operation permissions here.
if (config.unsupportedOperations?.has("lock")) {
return sendOperationNotSupported(response, "lock");
}
const owner = parseSingleField(body.bytes, "owner");
await storageService.lock(segments, owner);
response.status(204).set("Cache-Control", "no-store").end();
});

Response​

HTTP/1.1 204 No Content
ItemFormatDescription
Status204 No ContentA new lock was acquired or an existing lock for the same owner was confirmed. Other 2xx statuses also succeed
BodyNoneNo lock token or result JSON is required. Bodies of other 2xx responses contain no fields required to determine success
StatusConditions and handling
400 Bad RequestMissing owner, empty string, incorrect type, or extra JSON fields
409 ConflictAnother owner holds the lock. Do not change the existing lock
501 Not ImplementedAfter authentication, LOCK_NOT_SUPPORTED. Office treats this as successful without an actual lock

If lock and unlock are unsupported, return the unsupported operation response.

unlock - Unlock a document​

unlock releases the lock held by the requested owner. It succeeds if the lock is already absent. If a different owner holds the lock, return 409 Conflict. Implement it together with lock.

Request​

POST /tfo-http-storage/v1/{path}/unlock

POST /tfo-http-storage/v1/contracts/sample.docx/unlock HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 25

{
"owner": "editor-1"
}
ItemFormatDescription
pathPath · stringRelative path of the document to unlock
Authentication headersRequired · stringJWT for this POST request. See Common headers
Content-TypeRequired headerExactly application/json
Content-LengthRequired headerUTF-8 byte length of the JSON body
ownerBody · string · requiredThe same owner identifier used to acquire the lock; the only JSON field

Implementation example​

Shared helpers and storage service used by this snippet: Node.js Provider source

// POST /tfo-http-storage/v1/{path}/unlock
router.post(/^\/tfo-http-storage\/v1\/(?:.+\/)?unlock$/, async function unlockDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// Strip the prefix and operation from the raw path; decode each directory/file segment.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/unlock".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// Read the original JSON bytes and calculate their length and SHA-256.
const body = await readRequestBody(request, "json", config, stateStore);
// Verify the JWT signature and validity period, match the request details, and check for replay.
// Read X-TFO-Storage-Request-JWT from request.headers.
// Compare the received length/SHA-256 in body with the signed JWT values to detect body tampering.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// For customer sessions, validate the token in client_metadata and check document/operation permissions here.
if (config.unsupportedOperations?.has("unlock")) {
return sendOperationNotSupported(response, "unlock");
}
const owner = parseSingleField(body.bytes, "owner");
await storageService.unlock(segments, owner);
response.status(204).set("Cache-Control", "no-store").end();
});

Response​

HTTP/1.1 204 No Content
ItemFormatDescription
Status204 No ContentThe same owner's lock was released, or no lock remains. Other 2xx statuses also succeed
BodyNoneNo unlock result JSON is required
StatusConditions and handling
400 Bad RequestMissing owner, empty string, incorrect type, or extra JSON fields
409 ConflictThe current lock owner differs. Preserve the existing lock
501 Not ImplementedAfter authentication, UNLOCK_NOT_SUPPORTED. Office treats this as successful without an actual unlock operation

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-http-storage/v1/{parent}/mkdir

POST /tfo-http-storage/v1/contracts/mkdir HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 23

{
"name": "archive"
}
ItemFormatDescription
parentPath · stringRelative path of an existing parent directory. Use /tfo-http-storage/v1/mkdir to create a directory under the root
Authentication headersRequired · stringJWT for this POST request. See Common headers
Content-TypeRequired headerExactly application/json
Content-LengthRequired headerUTF-8 byte length of the JSON body
nameBody · string · requiredOne 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.

Implementation example​

Shared helpers and storage service used by this snippet: Node.js Provider source

// POST /tfo-http-storage/v1/{path}/mkdir
router.post(/^\/tfo-http-storage\/v1\/(?:.+\/)?mkdir$/, async function mkdirDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// Strip the prefix and operation from the raw path; decode each directory/file segment.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/mkdir".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// Read the original JSON bytes and calculate their length and SHA-256.
const body = await readRequestBody(request, "json", config, stateStore);
// Verify the JWT signature and validity period, match the request details, and check for replay.
// Read X-TFO-Storage-Request-JWT from request.headers.
// Compare the received length/SHA-256 in body with the signed JWT values to detect body tampering.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// For customer sessions, validate the token in client_metadata and check document/operation permissions here.
if (config.unsupportedOperations?.has("mkdir")) {
return sendOperationNotSupported(response, "mkdir");
}
const name = requireChildName(parseSingleField(body.bytes, "name"));
await storageService.createDirectory(segments, name);
response.status(204).set("Cache-Control", "no-store").end();
});

Response​

HTTP/1.1 204 No Content
ItemFormatDescription
Status204 No ContentThe parent/name directory was created. Other 2xx statuses also succeed
BodyNoneNo created entry object is required. Retrieve it later with info or list
StatusConditions and handling
400 Bad RequestMissing name, incorrect type, extra fields, or an invalid name
501 Not ImplementedAfter authentication, MKDIR_NOT_SUPPORTED. No directory is created

rename - Rename an entry​

This optional endpoint renames a file or directory within the same parent directory.

Request​

POST /tfo-http-storage/v1/{path}/rename

POST /tfo-http-storage/v1/contracts/sample.docx/rename HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 29

{
"name": "reviewed.docx"
}
ItemFormatDescription
pathPath · stringRelative path of the existing entry to rename. The Provider root cannot be renamed
Authentication headersRequired · stringJWT for this POST request. See Common headers
Content-TypeRequired headerExactly application/json
Content-LengthRequired headerUTF-8 byte length of the JSON body
nameBody · string · requiredOne new name, 1–255 characters; the only JSON field

Use the actual decoded name for name, following the mkdir name rules. Sending reviewed.docx for contracts/sample.docx results in contracts/reviewed.docx.

Implementation example​

Shared helpers and storage service used by this snippet: Node.js Provider source

// POST /tfo-http-storage/v1/{path}/rename
router.post(/^\/tfo-http-storage\/v1\/(?:.+\/)?rename$/, async function renameDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// Strip the prefix and operation from the raw path; decode each directory/file segment.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/rename".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// Read the original JSON bytes and calculate their length and SHA-256.
const body = await readRequestBody(request, "json", config, stateStore);
// Verify the JWT signature and validity period, match the request details, and check for replay.
// Read X-TFO-Storage-Request-JWT from request.headers.
// Compare the received length/SHA-256 in body with the signed JWT values to detect body tampering.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// For customer sessions, validate the token in client_metadata and check document/operation permissions here.
if (config.unsupportedOperations?.has("rename")) {
return sendOperationNotSupported(response, "rename");
}
const name = requireChildName(parseSingleField(body.bytes, "name"));
await storageService.rename(segments, name);
response.status(204).set("Cache-Control", "no-store").end();
});

Response​

HTTP/1.1 204 No Content
ItemFormatDescription
Status204 No ContentThe entry was renamed within the same parent. Other 2xx statuses also succeed
BodyNoneNo new path or entry object is required. Use info on the new path or list on its parent to retrieve it
StatusConditions and handling
400 Bad RequestInvalid name, extra JSON fields, or an attempt to rename the Provider root
404 Not FoundNo entry exists at the original path
409 ConflictThe new name is already in use
501 Not ImplementedAfter authentication, RENAME_NOT_SUPPORTED. The original name is preserved

delete - Delete an entry​

This optional endpoint deletes a file or directory. Reject attempts to delete the Provider root.

Request​

DELETE /tfo-http-storage/v1/{path}/delete

DELETE /tfo-http-storage/v1/contracts/reviewed.docx/delete HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
ItemFormatDescription
pathPath · stringRelative path of the entry to delete. The Provider root cannot be deleted
Authentication headersRequired · stringJWT for this DELETE request. See Common headers
Request bodyNoneOmit Content-Type. Follow the common request rules

Implementation example​

Shared helpers and storage service used by this snippet: Node.js Provider source

// DELETE /tfo-http-storage/v1/{path}/delete
router.delete(/^\/tfo-http-storage\/v1\/(?:.+\/)?delete$/, async function deleteDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// Strip the prefix and operation from the raw path; decode each directory/file segment.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/delete".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// Require no body. Use length 0 and the SHA-256 of the empty body.
const body = await readRequestBody(request, "none", config, stateStore);
// Verify the JWT signature and validity period, match the request details, and check for replay.
// Read X-TFO-Storage-Request-JWT from request.headers.
// Even without a body, length 0 and the empty-body SHA-256 must match the JWT.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// For customer sessions, validate the token in client_metadata and check document/operation permissions here.
if (config.unsupportedOperations?.has("delete")) {
return sendOperationNotSupported(response, "delete");
}
await storageService.delete(segments);
response.status(204).set("Cache-Control", "no-store").end();
});

Response​

HTTP/1.1 204 No Content
ItemFormatDescription
Status204 No ContentThe entry was deleted. Other 2xx statuses also succeed
BodyNoneNo deleted entry object or deletion count is required
StatusConditions and handling
400 Bad RequestAttempt to delete the Provider root
501 Not ImplementedAfter authentication, 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.

ItemFormatDescription
StatusRequiredExactly 501 Not Implemented
Content-TypeRequired headerapplication/json; a charset parameter is allowed
codeBody · string · requiredThe exact value for the current operation from the table below. No additional fields
Body lengthBrief UTF-8 JSONThe 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: 34

{
"code": "LIST_NOT_SUPPORTED"
}
OperationExact codeOffice behavior
listLIST_NOT_SUPPORTEDFile listing is unavailable. The connection can be saved after acknowledging the notice during registration
putPUT_NOT_SUPPORTEDSaving fails
lockLOCK_NOT_SUPPORTEDTreated as successful without an actual lock
unlockUNLOCK_NOT_SUPPORTEDTreated as successful without an actual unlock
mkdirMKDIR_NOT_SUPPORTEDDirectory creation is unavailable
renameRENAME_NOT_SUPPORTEDRenaming is unavailable
deleteDELETE_NOT_SUPPORTEDDeletion is unavailable

Because info and get are required, a 501 fails those operations. For optional operations, Office also reports failure if the status, media type, or body does not match the format above.

Among unsupported-operation responses, only those for lock and unlock are treated as successful by Office. Declare both operations unsupported and return LOCK_NOT_SUPPORTED or UNLOCK_NOT_SUPPORTED for the corresponding request. Do not implement only one of the pair. No document lock is established in this case, so the Provider must define how it handles overwrites or conflicts from concurrent saves.

For implemented lock and unlock operations, the normal response is 204. Office treats 409 Conflict, authentication failures, network errors, and storage failures as operation 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.

ItemFormatDescription
StatusHTTP status codeChoose according to the error cause below. The status determines success or failure
Content-TypeMatch the bodyNo fixed media type for ordinary errors
Bodytext or Provider-defined JSONOptional description of the error

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
StatusWhen to use
400 Bad RequestInvalid path, JSON, body length, or a disallowed query
401 UnauthorizedUnknown adapter, invalid signature or claims, or an expired or reused JWT
403 ForbiddenNo permission to access the storage or document
404 Not FoundTarget entry or request path does not exist
405 Method Not AllowedIncorrect method for a known operation path
409 ConflictName, type, or lock-owner conflict
411 Length RequiredRequired body Content-Length is missing
413 Payload Too LargeRequest body, document, or listing exceeds its limit, or a Provider-defined limit is exceeded
415 Unsupported Media TypeRequest Content-Type does not match the operation's required media type
500 Internal Server ErrorInternal Provider error. Do not expose exception details in the response
501 Not ImplementedExact unsupported declaration for an optional operation as specified above
503 Service UnavailableTemporary storage failure

Do not turn authentication errors into unsupported responses or report storage failures as successful. See Run a Provider example for example-specific configuration.