Skip to content

Storage integration

Discovery and capabilities

Read /hosting/discovery to get urlsrc per file extension and /hosting/capabilities to learn what the editor server can do.

On this page

The editor server describes itself through two public endpoints, served through the O3O proxy without authentication. A WOPI host reads them to learn which URL opens each file type and what the server can do.

GET/hosting/discovery

List of actions per file type, as XML.

Auth: noneCommunityEnterprise
GET/hosting/capabilities

Editor server capabilities, as JSON.

Auth: noneCommunityEnterprise

Discovery structure#

The root is <wopi-discovery> containing one <net-zone name="external-http">. Each <app> is a group: by application (writer, calc, impress, draw) or by MIME type. Each <action> has ext (file extension, empty for MIME entries), name (edit, view, editnew) and urlsrc.

HTTPExcerpt of the DEV discovery (trimmed)
HTTP/1.1 200 OK
Content-Type: text/xml

<wopi-discovery>
    <net-zone name="external-http">
        <app favIconUrl="http://localhost:8080/browser/825c9caa93/images/x-office-document.svg" name="writer">
            <action default="true" ext="odt" name="edit" urlsrc="http://localhost:8080/browser/825c9caa93/cool.html?"/>
            <action ext="odt" name="view" urlsrc="http://localhost:8080/browser/825c9caa93/cool.html?"/>
            <action default="true" ext="docx" name="edit" urlsrc="http://localhost:8080/browser/825c9caa93/cool.html?"/>
            <action ext="docx" name="view" urlsrc="http://localhost:8080/browser/825c9caa93/cool.html?"/>
            <action ext="docx" name="editnew" urlsrc="http://localhost:8080/browser/825c9caa93/cool.html?"/>
            <!-- ... -->
        </app>
        <app name="calc"> <!-- xlsx, xls, ods, csv ... --> </app>
        <app name="impress"> <!-- pptx, ppt, odp ... --> </app>
        <app name="application/vnd.openxmlformats-officedocument.wordprocessingml.document">
            <action default="true" ext="" name="edit" urlsrc="http://localhost:8080/browser/825c9caa93/cool.html?"/>
            <action default="true" ext="" name="view" urlsrc="http://localhost:8080/browser/825c9caa93/cool.html?"/>
        </app>
        <!-- ... -->
        <app name="Capabilities">
            <action ext="" name="getinfo" urlsrc="http://localhost:8080/hosting/capabilities"/>
        </app>
    </net-zone>
</wopi-discovery>
  • urlsrc ends with ?. Append WOPISrc=<encoded URL> and optional parameters such as lang=vi.
  • The origin of urlsrc comes from O3O_ONLINE_SERVER_NAME; the scheme is https when O3O_ONLINE_SSL_TERMINATION=true. It does not depend on the address you used to call discovery.
  • The /browser/<hash>/ segment changes with the server version. Always read discovery again; never hard-code it. Caching for a few minutes is enough.
  • The editor server opens extensions such as docx, doc, odt, rtf, txt, xlsx, xls, ods, csv, pptx, ppt, odp and more; the exact list is the set of ext with name="edit". The O3O.Editor embed layer accepts only the 12 extensions listed in the configuration reference.
Look up urlsrc by extension
# Look up urlsrc by file extension from discovery (standard library only)
import urllib.request
import xml.etree.ElementTree as ET

O3O_URL = "http://localhost:8080"


def load_actions():
    xml = urllib.request.urlopen(O3O_URL + "/hosting/discovery", timeout=10).read()
    actions = {}
    for action in ET.fromstring(xml).iter("action"):
        ext, name = action.get("ext"), action.get("name")
        if ext:  # skip MIME-type entries (empty ext)
            actions.setdefault((ext, name), action.get("urlsrc"))
    return actions


actions = load_actions()
print(actions[("docx", "edit")])   # http://localhost:8080/browser/<hash>/cool.html?
print(actions[("xlsx", "view")])
print(sorted({ext for ext, name in actions if name == "edit"}))
// Node.js 18+ (built-in fetch). Cache the result for a few minutes instead of calling it on every open.
const O3O_URL = "http://localhost:8080";

async function urlsrcFor(ext, action = "edit") {
  const xml = await (await fetch(`${O3O_URL}/hosting/discovery`)).text();
  for (const tag of xml.match(/<action\b[^>]*>/g) || []) {
    const attr = (n) => (tag.match(new RegExp(`\\b${n}="([^"]*)"`)) || [])[1];
    if (attr("ext") === ext && attr("name") === action) return attr("urlsrc");
  }
  throw new Error(`no ${action} action for .${ext}`);
}

urlsrcFor("docx").then(console.log);   // http://localhost:8080/browser/<hash>/cool.html?

Capabilities#

200GET /hosting/capabilities on the DEV bundle (trimmed)
{
  "convert-to": {
    "available": true,
    "endpoint": "/cool/convert-to"
  },
  "hasMobileSupport": true,
  "hasProxyPrefix": false,
  "hasSettingIframeSupport": true,
  "hasTemplateSource": true,
  "hasWopiAccessCheck": true,
  "productName": "…",
  "productVersion": "26.04.4.1",
  "productVersionHash": "825c9caa93",
  "serverId": "CDF2D4FC"
}
FieldMeaning
productName, productVersionName and version of the editor server. The DEV image returns the upstream name; the source-built O3O image (coming soon) will return the O3O name. Do not switch features on this value.
convert-toThe editor server reports a conversion endpoint, but with O3O that path is NOT usable from outside: o3o-proxy returns 404 for /cool/convert-to and /lool/convert-to. Only a Nextcloud on the same Docker network, calling o3o-online:9980 directly, uses it to render previews. To convert files, call DocBuilder's POST /v1/convert: it has authentication, plan limits and does not count as a connection.
hasMobileSupport, hasWopiAccessCheck, …Capability flags; a WOPI host may read them to adapt its own interface.
Coming soon

The <proof-key> element in discovery (public key to verify X-WOPI-Proof). Not present in v1.