Getting started

API keys, your first call, and what counts as a breaking change. Everything you need before integrating with ez.fyi from an ERP, billing system, or customer portal.

Authentication

Every /api/v1/* request requires a Bearer token. Tokens are issued by OCPL IT — your contact at OCPL provisions one for you and delivers it over a secure channel. Send it on every request:

Authorization: Bearer ezfy_xxxxxxxxxxxxxxxxxxxxxxxxxx

Keep the key secret; it's your full credential. If it leaks, ask OCPL to rotate it — the old key is revoked instantly and a new one issued in the same minute.

Your first API call

Create a static-file short link. Paste in the path of a file already in the OCPL S3 bucket (your contact will give you the path), and the response will tell you the recipient-facing short URL.

curl -X POST https://ez.fyi/api/v1/links \
  -H "Authorization: Bearer ezfy_..." \
  -H "Content-Type: application/json" \
  -d '{
    "file_ref": "links/2026/06/q2-invoice.pdf",
    "content_type": "application/pdf",
    "label": "Q2 invoice",
    "tags": ["doc-type:invoice", "fiscal-year:2026-27"]
  }'

The response contains the slug, the full URL, the creation timestamp, and the canonical (lower-cased, deduped) tag list:

{
  "slug": "aB3_x9Qz2k",
  "url": "https://ez.fyi/aB3_x9Qz2k",
  "created_at": "2026-06-01T08:14:23.402Z",
  "tags": ["doc-type:invoice", "fiscal-year:2026-27"]
}

Send the url to the recipient through your own channel (email, WhatsApp, SMS). When they open it, the file streams from S3 with the URL bar staying on https://ez.fyi/aB3_x9Qz2k — no redirect, no internal storage path leaks.

For Postman or Bruno, paste /api/v1/openapi.json into your tool's "Import OpenAPI" dialog — both tools generate a working collection in one click.

Redirect links

A redirect link sends the recipient on to another page: opening https://ez.fyi/<slug> answers 302 with a Location header. Send "type": "redirect" and a redirect_url. The URL must be https, must not contain a user name or password, must use the default port, and its host must be on the allowlist OCPL has configured for the environment. Anything else is refused with 400 and the code invalid_redirect_url or redirect_host_not_allowed.

curl -X POST https://ez.fyi/api/v1/links \
  -H "Authorization: Bearer ezfy_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "redirect",
    "redirect_url": "https://app.example.com/orders/1042",
    "label": "Order 1042",
    "tags": ["app:orders"]
  }'

The response is the same as for a file link (slug, url, created_at, tags) and the slug is generated. label and tags are optional and behave as on file links. A redirect body accepts only type, redirect_url, slug, label, tags and expires_at (null only, see below); anything else is 422 unknown_field.

Choosing the slug

Integrations that OCPL has enabled for a slug prefix (today wf-) may also send "slug": the prefix followed by exactly 8 lowercase hex characters, 11 characters in all, for example wf-3fa9c01b. A consumer without a prefix that sends slug gets 403 slug_not_allowed; a slug of the wrong shape gets 400 invalid_slug; a slug that already exists, even a revoked one, gets 409 slug_taken, so generate a new one and retry. Generated slugs never start with wf-. DELETE /api/v1/links/{slug} and the tag endpoints accept both slug forms.

Link expiry is not implemented yet, so links never expire. A redirect or live content body may carry "expires_at": null (it means "never expires" and changes nothing; leaving it out means the same). Any other value, such as a timestamp or a string, is refused with 422 invalid_field until expiry exists. File links do not accept expires_at at all.

Live content links

A live content link makes the address https://ez.fyi/<slug> show what one of your own APIs returns at the moment someone opens it, with the address bar staying on ez.fyi. Send "type": "dynamic" and an api_url. OCPL IT must first allow the API's host and give your integration an upstream token for it; until then creation is refused with 400 api_host_not_allowed (the host is not allowed, or is not the host your token is for) or 403 no_upstream_token. The URL must be https on the default port, with no user name or password, and its host must be a domain name (an IP address is refused with 400 invalid_api_url).

curl -X POST https://ez.fyi/api/v1/links \
  -H "Authorization: Bearer ezfy_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "dynamic",
    "api_url": "https://api.example.com/invoices/1042/pdf",
    "label": "Invoice 1042 (live)",
    "tags": ["doc-type:invoice"]
  }'

The response is the same as for other links, and slug may be chosen exactly as for redirect links. When someone opens the short URL, ez.fyi calls your api_url with GET, sending Authorization: Bearer <your upstream token> and X-Ezfyi-Slug: <slug> and nothing from the visitor (no cookies, no browser headers). Redirects are never followed and the answer is never cached. Anyone who knows a slug can open it, so your API should decide what each slug may see: authorise on X-Ezfyi-Slug.

What the visitor gets back: only a 200 is shown. Its body is streamed through with just your Content-Type, Content-Disposition and Content-Length (plus Cache-Control: private, no-store, Referrer-Policy: no-referrer and X-Content-Type-Options: nosniff), up to 25 MB; a body that grows past that while streaming is cut off. ez.fyi shows PDFs, PNG, JPEG, GIF and WebP images, plain text and JSON in the browser; any other type (HTML and SVG included) is always sent as a download, whatever your Content-Disposition says, because ez.fyi will not run another system's pages under its own address. Your 404 shows ez.fyi's not-found page and your 410 its retired page. Any other status, no answer within 10 seconds, or a declared size over 25 MB shows a "temporarily unavailable" page (502 or 504). The accepted body fields are type, api_url, slug, label, tags and expires_at (null only, as for redirect links).

Versioning & stability

v1 is stable. Additive changes (new optional fields, new endpoints, new error codes) ship without notice. Removals or renames will only happen under a new major version (/api/v2/*), with at least 6 months of overlap during which both versions work. Endpoints scheduled for removal are flagged with deprecated: true in the OpenAPI document.

The full per-change history lives at /docs/changelog; the project narrative lives at /backlogs.