Skip to main content

Any API with an OpenAPI document

Many APIs describe themselves in an OpenAPI document: a JSON or YAML file listing each endpoint, the method it answers, the parameters it takes and how a caller authenticates. If the API you want is not in the catalog but publishes one, your gateway can build a connection from it. You give the API's address, review the capabilities the document produces, add the credential, and connect.

There is no connector to wait for, so this covers a SaaS API that publishes its spec as well as a service of your own, as long as it is reachable on the public internet. For an API with no document at all, see Any other web API.

What you need​

  • The API's address, the one its requests go to, such as https://api.example.com.
  • An OpenAPI 3.0 or 3.1 document, in JSON or YAML, served from that same host. A Swagger 2.0 document is refused; convert it to OpenAPI 3 first. If the document lives on a different host, such as a docs site or GitHub, see When the document is somewhere else.
  • A credential for the API: an API key or token, or a username and password for one that uses HTTP Basic.

The API has to be served over https://. Your gateway refuses http://, localhost, private and link-local addresses and cloud metadata hosts, for the document and for every call, on a gateway you run yourself as well as a hosted one, so a service that is only reachable on your own network cannot be connected this way.

Add the connection​

In the console, open Access → Connections → Add connection and choose Connect your own. Enter the API's address as the Bound URL and choose Discover. It is called bound because the connection can only ever reach this host.

Your gateway checks whether the address is a WordPress site first, then looks for an OpenAPI document in this order:

  1. The address itself, if it ends in .json, .yaml or .yml.
  2. /openapi.json, /openapi.yaml, /swagger.json, /v3/api-docs and /.well-known/openapi.json, under the address's own path.
  3. The same five at the root of the host.

If none of them answers, the console lists what it tried and asks for the OpenAPI document URL. Enter the document's address and choose Try it. It has to be on the same host as the bound URL, though a bare www. difference is allowed.

The review then shows where the document was found, the server the connection will be pinned to, and every capability the document produces, in two columns: Enabled on connect and Needs your approval. Hover one to see its method and path. Anything the importer could not use, such as an operation it skipped, is listed underneath as a warning. If the document lists more than one server, a production and a sandbox one say, choose one under Server: the connection is pinned to the server you pick, and you can add a second connection for the other.

Give the connection a Name. The console suggests one from the host, and the name also becomes the connection's id, which prefixes every capability it has: Acme API becomes acme_api, so its capabilities are acme_api.listInvoices and so on. Under Connect with, the console says which scheme it detected in the document and asks for the matching credential (see How the credential is sent). You can pick grants here too, and the console attaches a read-only starter policy to them, which allows only the reads that are enabled and ask for no approval. Then choose Connect. The credential is sealed on your gateway and never displayed again.

The capability list you reviewed is what gets stored. The gateway creates the connection only against the document you reviewed, so if the API's document changes between your review and connecting, the console says the API has changed and shows you the new review. Nothing re-reads the document when an agent makes a call, which means a new endpoint in the API cannot give an agent a new action on its own. See Re-sync for how to pick up changes.

Which server it is pinned to​

An agent never supplies an address. It fills in the parameters a capability declares, and the gateway builds the request against the one server the connection is pinned to, which comes from the document's servers list:

  • A {variable} in a server's URL takes its default. A server with a variable that has no default cannot be used.
  • A relative server, such as /v2, is resolved against the document's own address.
  • A document with no servers at all is pinned to the root of the host it was fetched from.
  • Every server has to be https:// and on the bound host. One that names another host is refused, so a document cannot point the connection somewhere you did not name.
  • servers set on a single path or operation are ignored, with a warning. Every capability uses the connection's one server.

What the agent can do​

Each operation becomes one capability, named after the connection's id and the operation's operationId:

OperationCapability
GET /invoices, operationId listInvoicesacme_api.listInvoices
GET /invoices/{id}, operationId get-invoiceacme_api.get_invoice
GET /pets/{id}, with no operationIdacme_api.get_petsid

An operationId keeps its letters, digits and underscores, a hyphen becomes an underscore, and anything else is dropped. An operation without one is named from its method and path. If two operations would end up with the same name, the second gets _2, and so on, with a warning. GET, POST, PUT, PATCH and DELETE are imported; HEAD, OPTIONS and TRACE are skipped.

What the operation doesRiskWhen you connect
Reads (GET)MediumOn
A read whose operationId or path names exportHighOff
Creates (POST)HighOff
Updates (PUT, PATCH)HighOff
Deletes (DELETE)CriticalOff, and asks for approval on every call
A write whose path or operationId contains refund, charge, capture, payout or transfer, even inside a longer wordCriticalOff, and asks for approval on every call
A read whose path has trash, delete, destroy, purge, remove, flush, reset, wipe, clear, empty, revoke, deactivate, disable, uninstall, cancel, invalidate, logout, disconnect, unsubscribe, regenerate or trigger as a wordCriticalOff, and asks for approval on every call

Once you turn a high-risk capability on, the gateway still holds each call for approval, whatever a policy says, as it does for every connection. Everything in the table comes from the operation's method and the words in its path and operationId, both of which the API's author writes, so they can only make a capability riskier. They cannot know that an innocently named POST /adjustments refunds a payment, which is one reason writes start switched off: read what each one does before you turn it on. If a rule gates something you know is safe, the gateway's owner can lower it one capability at a time, as described in Running a capability without its approval.

Only the parameters the document declares travel. Those are the operation's path, query and header parameters and the top-level properties of a JSON body (or of a form body, when there is no JSON one). Anything else an agent sends is dropped before the request is built. Cookie parameters are not sent, the document's own Authorization, Accept and Content-Type headers are ignored, and no agent can set the header or query parameter the credential travels in. The types and ranges in the schema are not enforced, so the API's own validation is what checks a value.

References inside the document ($ref to #/components/...) are followed. A reference to another file or a URL is never fetched, and whatever uses one is skipped with a warning. A request body defined with oneOf or anyOf gets no body parameters, also with a warning.

How the credential is sent​

The scheme comes from the document's top-level security requirement or, when it has none, from its components.securitySchemes:

The document declaresThe console asks forThe gateway sends
http with scheme bearerA bearer tokenAuthorization: Bearer <token>
http with scheme basicA username and passwordHTTP Basic
apiKey in a headerAn API keyThe key in that header
apiKey in the queryAn API keyThe key as that query parameter
oauth2 or openIdConnectA bearer tokenAuthorization: Bearer <token>, with a warning
apiKey in a cookie, or no scheme at allA bearer tokenAuthorization: Bearer <token>, with a warning

For an OAuth API, seal an access token that does not expire, such as a personal or service token, if the API issues one: the gateway sends the token you give it and never refreshes it. An API key sent as a cookie is not supported.

The console cannot change the scheme it detected. If the document gets it wrong, say it declares nothing for an API that expects its key in X-Api-Key, import it through the REST API with an auth override, as the next section shows.

When the document is somewhere else​

Plenty of APIs publish their document away from the API itself: on a docs site, in a GitHub repository, or only as a download. The console reads a document only from the bound host, so for these, and for a document you have trimmed or written yourself, import it through the gateway's REST API instead, pasting the document as spec and naming the server with base_url. This needs the gateway's operator token, which a gateway you run yourself keeps in ~/.permaura/operator.token; on a hosted gateway the console is the only way in for now.

Run it as a dry run first, which reviews the document without storing anything:

TOK=$(cat ~/.permaura/operator.token)

jq -n --rawfile spec acme-openapi.yaml \
'{name: "Acme API", spec: $spec, base_url: "https://api.acme.example/v2", dry_run: true}' > acme-import.json

curl -s -X POST http://127.0.0.1:7376/v1/connections/import-openapi \
-H @<(printf 'authorization: Bearer %s\n' "$TOK") -H 'content-type: application/json' \
-d @acme-import.json | jq '{base_url, auth, warnings, capabilities: [.capabilities[] | {id, method, path, risk, disabled}]}'

The -H @<(…) form keeps the token out of curl's arguments, where every local user can read it in the process list; it needs bash or zsh. Send the same body without dry_run to create the connection, then seal the credential with POST /v1/connections/acme_api/credential. Any other web API has that call.

To change how the credential is sent, add an auth field to the body:

authSends the credential as
{"scheme": "bearer"}Authorization: Bearer <credential>
{"scheme": "basic"}HTTP Basic, with the credential written as username:password
{"scheme": "basic", "key_as_username": true}HTTP Basic, with the credential as the username and an empty password
{"scheme": "api_key_header", "header": "X-Api-Key"}The credential in that header
{"scheme": "api_key_query", "param": "api_key"}The credential as that query parameter
{"scheme": "raw", "header": "Authorization", "prefix": "Token "}That header, with the prefix written before the credential

A pasted document has no address to read again, so a connection made this way cannot be re-synced. To pick up a new version, import it as a new connection.

Re-sync​

The connection page has a Source card showing the document the connection was built from. Re-sync reads it again, keeping the server and the scheme the connection is pinned to, and shows what changed before anything is applied:

  • New capabilities are added switched off, reads included.
  • Changed capabilities are updated and switched off so you can review them again.
  • Removed capabilities are kept and flagged, never deleted, so a policy that names one fails loudly at review rather than quietly at call time.

A capability whose risk the owner lowered keeps that setting while the document still describes it the same way.

Limits​

  • One import turns at most 2,000 operations into capabilities.
  • A document your gateway fetches can be at most 5 MiB, and a pasted one has to fit in an 8 MiB request.
  • All of a gateway's capabilities together may take at most 8 MiB on a hosted gateway and 64 MiB on one you run yourself.

An import past any of these is refused before anything is stored. On a gateway you run yourself, PERMAURA_LIMIT_OPENAPI_OPERATIONS, PERMAURA_HTTP_MAX_RESPONSE_BYTES, PERMAURA_LIMIT_BODY_BULK_BYTES and PERMAURA_LIMIT_CAPABILITY_BYTES move them. A very large API is usually better trimmed to the endpoints an agent needs anyway, since a short list is easier to review and to grant.

Good to know​

  • "The API changed since you reviewed it." The document changed between the review and Connect. Look over the new review and connect again. Over the REST API this is a 409 carrying the new digest.
  • Every server is refused. The document's servers are on a different host from the bound URL, which is common when the document is published on a docs site. Import it through the REST API with base_url, as above.
  • "Connection already exists" over the REST API. The name makes an id another connection already has, and the import refuses it even as a dry run. Choose another name, or send an id of your own. The console avoids this by suggesting a name nothing uses yet.
  • Long or odd capability names. A document without operationIds gets names like get_petsid. They work, but they are harder to read in a policy, so a document you control is worth giving operationIds.
  • Writes are off after you connect. That is deliberate. Turn on the ones an agent needs from the connection page, and they still answer to your grants and policies.