API

A contract-first REST API, its conventions and how CI keeps code and contract together.

Everything the Secure File Transfer web app does goes through a public, versioned REST API, and tenant automation uses the same API with API keys. The contract is written first in OpenAPI 3.1, and CI keeps the code in step with it.

Contract first

The requirement (FR-31) is that every web app action goes through a public REST API described in OpenAPI 3.1. The project went further and wrote the contract before any code (ADR-0002). The file docs/api/openapi.yaml leads, and the code follows.

Four CI checks hold the two together:

CheckFails when
LintThe spec breaks the linter's rules, or an operation lacks a declared permission or a 429 response
Generated specAn operation the code implements is missing from the contract, or differs in method, path, parameters, request body or any response
Breaking changesA pull request changes the contract in a way that breaks clients, and doesn't carry the breaking label
Web app clientThe web app's TypeScript client, generated from the contract, is out of date or no longer type-checks

The generated-spec check builds the OpenAPI document FastAPI produces and compares each operation with the contract, error responses included, in both directions. Operations not yet implemented are allowed, and the check prints how many remain. The breaking-change check runs oasdiff against the base branch, with no network access, because the pull request's file is untrusted.

Operations

The contract has 82 operations under the /v1 base path, each in one tag.

TagOperationsCovers
Data loss prevention22DLP policy, rules and detectors; findings; confirmation; approvals; reports
Tenant admin10Members, roles, tenant policy, email suppressions
Transfers9Create, send, cancel, revoke, extend; recipients and unlocks
Recipient session9What a verified recipient or uploader can list, upload and download
Files8Declaring files, part URLs, completion; received-file downloads
Upload requests6Create, list, revoke, unlock; received files
Operator5Operator token, tenant provisioning, SSO setup, admin recovery, break-glass records
Sign-in4Start and complete sign-in, invitation sign-in, sign-out
Links and verification3Resolve a link, request a code, open a link session
API keys3Create, list, revoke
Audit2Search and export
Account1The signed-in member

The operator operations are served by a separate operator API that no load balancer routes to. Operators reach it by port-forward.

Authentication

Every operation declares its security scheme and the permission it checks. A request that carries both a session cookie and an Authorization header is refused with 401 credentials_invalid.

CallerSends
Member or admin, web app__Host-sft_session cookie, plus X-CSRF-Token on POST, PATCH and DELETE
Tenant automationAn API key in Authorization: Bearer, never in a query string or body
Recipient or uploader, before verificationThe link token in the request body
Recipient or uploader, after verification__Host-sft_link cookie, X-Link-Session naming the session, and X-CSRF-Token
Platform operatorA 15-minute operator token in Authorization: Bearer

Only seven operations are public: the three link steps, the three sign-in steps, and the operator token exchange, whose signed AWS request is itself the credential. No token ever appears in a URL path or query string, because URLs end up in logs and browser history. See Recipient verification for the link flow.

Errors, pagination and idempotency

Errors

Every error is an RFC 9457 problem document (application/problem+json) with a stable machine-readable code and the request_id that also appears in the logs. Clients branch on code and the HTTP status, never on the human-readable text. An item in another tenant returns 404, never 403, so its existence is never revealed. A path ID that can't exist gets the same 404.

Pagination

List operations take cursor and limit (1 to 100, default 25) and return items and next_cursor, which is null on the last page. Cursors are opaque. Lists the requirements already bound, such as the 50 files in a transfer, are returned whole.

Idempotency

Every POST that creates something accepts an Idempotency-Key header. The key is kept for 24 hours with a fingerprint of the method, the path and the body. A retry with the same key and request gets the same status; the same key with a different request gets 409 idempotency_conflict. The service never stores a response body. Calls that grant access, such as download grants and verification codes, are checked and audited every time and never replay. State changes such as send and revoke are safe to retry by design.

Security-critical changes, such as revocations, take effect at once and are then written to the audit archive in both regions. If that write fails, the call returns 503, but the change stands, and a retry completes the record.

Rate limits

A request over a limit gets 429 rate_limited with a Retry-After header in whole seconds. The error never names the key, another tenant or another address. Every operation declares 429, and a lint rule enforces it.

LimitApplies to
1,000 API requests per tenant per minuteEvery tenant operation, members and API keys together. Revocations never count
60 link-page requests per IP per minuteThe three link steps
5 verification codes per email address per hourCode requests, across all tenants
300 requests per minute per member and per keyOther tenant calls
1 audit export an hour, at most 100,000 eventsEach API key

The WAF adds per-IP limits in front of the API. If the rate-limit counters are unavailable, the link and sign-in steps fail closed with 503, and other calls fall back to a per-pod limiter.

API keys

Tenant API keys (FR-32) serve automation such as creating transfers or exporting the audit log.

  • Format. sft_prd_<secret>_<check>: the environment, 256 random bits, and a checksum, so typos and random strings are rejected without a database lookup. The service stores only a SHA-256 hash, and the full key is shown once.
  • Created by a tenant admin in the web app only, with a recent sign-in. A key can't create a key.
  • Scopes are fixed at creation: transfers:read, transfers:write, upload_requests:read, upload_requests:write and audit:read.
  • Expiry is 90 days by default and at most 365. A tenant has at most 25 active keys.
  • Limits. A key never downloads file contents, unlocks recipients, decides DLP approvals or manages users, policy or keys. Deactivating or demoting its owner revokes it in the same transaction.
  • Audited. Every call a key makes, reads included, has its own audit record.

Versioning

The major version is in the path. Within /v1, only backwards-compatible changes are made: new operations, new optional request fields, new response fields and new error codes. Clients ignore response fields they don't know. A breaking change ships only as /v2, which runs alongside /v1 until /v1 is retired. A deprecated operation is marked in the spec, and its responses carry Deprecation and Sunset headers, with at least six months' notice for API-key automation.

Source: API contract, openapi.yaml, ADR-0002, Authentication and authorization, PRD