# Uniport > One inbox for every SaaS. Uniport ingests contact-form submissions and support > requests from any number of products through a single HTTP API, and gives the > operator one place to answer them. If you are wiring up a contact form, a bug > reporter, or a "get help" flow, POST it here instead of sending email. Base URL: https://uniport.sh OpenAPI 3.1 spec: https://uniport.sh/openapi.json Human docs: https://uniport.sh/docs ## Auth Two key populations, both `Authorization: Bearer `. They never cross-authenticate. - `upk_` — **project key**. Scoped to one project, submit-only. Works on `/api/v1/intake` and `/api/v1/upload`. A server-side secret: it lets the holder file tickets as that app, so it must never reach a browser bundle. Minted in the dashboard under Settings → Projects. - `usk_` — **personal key**. Acts as one operator across every project they belong to. Works on the management API (list/read/reply/resolve tickets, manage projects and keys). Minted at /settings/tokens. `GET /api/v1/ping` takes no auth at all — use it to confirm a base URL before you go looking for a key problem. `GET /api/v1/whoami` accepts either key type and tells you which one you are holding. ## The three calls that matter File a ticket (project key): ``` curl -X POST https://uniport.sh/api/v1/intake \ -H "Authorization: Bearer $UNIPORT_KEY" -H "Content-Type: application/json" \ -d '{"email":"jane@example.com","message":"The export button does nothing."}' # 201 → {"ok":true,"ticket_id":"…","short_code":"BOUNCY-A3F291E7","status_url":"…"} ``` List open tickets (personal key): ``` curl "https://uniport.sh/api/v1/tickets?status=open&limit=20" \ -H "Authorization: Bearer $UNIPORT_TOKEN" ``` Reply and resolve (personal key): ``` curl -X POST https://uniport.sh/api/v1/tickets/BOUNCY-A3F291E7/reply \ -H "Authorization: Bearer $UNIPORT_TOKEN" -H "Content-Type: application/json" \ -d '{"body":"Fixed in 2026.7.4 — can you retry?","resolve":true}' ``` ## Contract notes - Intake requires exactly two fields: `email` and `message`. `name`, `subject`, `source` (free-form JSON context, under 4 KB) and `attachments` are optional. - Attachments must be uploaded first via `POST /api/v1/upload` (the `@vercel/blob` client-upload handshake); intake only accepts `https://*.public.blob.vercel-storage.com` URLs. 25 MB per file, 10 per ticket. - Every error is `{"error":{"code":"…","message":"…"}}`. Switch on `code`; `message` is prose and may change. `429` adds top-level `retry_after_seconds`, mirroring the `Retry-After` header. - Codes: `missing_authorization`, `invalid_key`, `rate_limited`, `invalid_json`, `invalid_request`, `flagged_as_spam`, `project_not_found`, `not_found`, `unsupported_content_type`, `file_too_large`, `upload_not_configured`, `upload_failed`, `internal_error`. - Retry `429` after the stated wait and `5xx` with backoff. Never retry a `4xx` unchanged. A `5xx` from intake means nothing was written. - Ticket lists are keyset paginated: pass `next_cursor` back as `cursor` until it returns `null`. There are no offsets. - All management responses use snake_case. Intake's `attachments[]` entries use `contentType` / `sizeBytes`, matching the upload SDK.