# Authentication (/en/authentication) Send your API key as a bearer token on every request: ```bash curl https://api.upsky.dev/v1/platform/team \ -H "Authorization: Bearer $UPSKY_API_KEY" ``` ## Key types [#key-types] | Prefix | Environment | Use it for | | ------------- | ----------- | ----------------------------------------------- | | `upsky_test_` | Test | Development. No charges and no real checks run. | | `upsky_live_` | Live | Production traffic. | Keys belong to a team, so they keep working when a team member leaves. ## Keep keys safe [#keep-keys-safe] - Only use keys on your server. Never ship one in a browser or mobile app. - Store them in environment variables, not in your code. - Replace a key straight away if it may have leaked. ## Rotate a key [#rotate-a-key] Create a new key, deploy it, then delete the old one. You can have more than one active key, so there's no downtime. AI agents use OAuth instead of API keys. See [MCP authentication](/en/mcp/authentication). # Errors (/en/errors) Every API returns errors in the same shape: ```json { "error": { "code": "invalid_request", "message": "The field 'email' is required.", "request_id": "req_123" } } ``` Include `request_id` when you contact support. ## Status codes [#status-codes] | Status | Code | What to do | | ------ | ------------------------ | ------------------------------------------------------------- | | `400` | `invalid_request` | Fix the request. Retrying won't help. | | `401` | `unauthorized` | Check that your API key is correct and hasn't been revoked. | | `402` | `insufficient_credits` | Add credits or turn on auto top-up, then retry. | | `403` | `pro_required` | Subscribe to Pro. Live requests to paid products need it. | | `403` | `account_suspended` | Contact support. | | `403` | `forbidden` | Your key can't access this resource. | | `404` | `not_found` | Check the ID, and test versus live mode. | | `409` | `conflict` | The resource can't do that now, such as canceling a submitted verification. | | `409` | `idempotency_key_reused` | Use a new `Idempotency-Key` for a different request. | | `409` | `request_in_progress` | The first attempt is still running. Retry shortly. | | `409` | `retry_later` | Wait the seconds in `Retry-After`, then retry. | | `413` | `payload_too_large` | Send a body under 1 MB. | | `429` | `rate_limited` | Wait, then retry. See [limits](/en/rate-limits). | | `500` | `internal_error` | Retry with backoff. | | `503` | `service_unavailable` | Retry with backoff. | Branch on `code`, not on `message`: codes are stable, and messages may be reworded. Every response, successful or not, also carries the ID in an `X-Request-Id` header. ## Retrying safely [#retrying-safely] Add an `Idempotency-Key` header to `POST` requests. Any unique string up to 255 characters works, such as a UUID. - If the first request succeeded, a retry with the same key returns that same response, with an `Idempotent-Replayed: true` header, and the work isn't done twice. - If the first request was refused or failed, nothing is remembered, so a retry runs it again. - Keys are remembered for 24 hours, separately for test and live mode. # Introduction (/en) Upsky gives your app identity checks, AI models, file storage and hosting behind one account and one API key. ## Products [#products] Confirm who is behind every account. Call every AI model with one key. Store and serve your app's files. Deploy and run sites, from Next.js to WordPress. ## Connect to the platform [#connect-to-the-platform] Manage teams, domains, API keys and usage from your own code. Let AI agents work with your Upsky account. ## New here? [#new-here] Start with the [quickstart](/en/quickstart), then read how [authentication](/en/authentication) works. ## For AI tools [#for-ai-tools] Every page is available as Markdown by adding `.md` to its URL. The whole site is at [`/en/llms.txt`](/en/llms.txt) and [`/en/llms-full.txt`](/en/llms-full.txt). # Quickstart (/en/quickstart) ## 1. Create an API key [#1-create-an-api-key] In your Upsky dashboard, open your team's settings and create an API key. Copy a live key right away, because it's only shown once. A test key can be shown again from the list. ## 2. Save it as an environment variable [#2-save-it-as-an-environment-variable] ```bash export UPSKY_API_KEY="upsky_test_..." ``` ## 3. Make a request [#3-make-a-request] List the models available to your team: ```bash curl https://api.upsky.dev/v1/ai/models \ -H "Authorization: Bearer $UPSKY_API_KEY" ``` ## Next steps [#next-steps] - [Authentication](/en/authentication): test keys, live keys and how to rotate them - [Errors](/en/errors): what to do when a request fails - [Webhooks](/en/webhooks): hear about events as they happen # Rate limits and pagination (/en/rate-limits) ## Rate limits [#rate-limits] Limits apply to your whole team, so every key shares them: | Keys | Requests per minute | | ---- | ------------------- | | Live | 600 | | Test | 120 | Every response includes your current limit: | Header | Meaning | | ----------------------- | ---------------------------------------- | | `RateLimit-Limit` | Requests allowed in the current window | | `RateLimit-Remaining` | Requests left in the current window | | `RateLimit-Reset` | Seconds until the window resets | When you go over the limit you get a `429` with a `Retry-After` header. Wait that many seconds before trying again. ## Pagination [#pagination] List endpoints return a page of results and a cursor: ```json { "data": [], "next_cursor": "cur_123" } ``` Pass `next_cursor` as `?cursor=` to get the next page. When it's `null`, you've reached the end. Use `?limit=` to set the page size, up to 100. # Webhooks (/en/webhooks) Add an endpoint in your dashboard, under **Team → Webhooks**, and Upsky will `POST` events to it as they happen. Each endpoint is for test or live mode and gets the events of that mode only. ```json { "id": "6b2c9f3e-1a4d-4e5f-9b8a-7c6d5e4f3a2b", "object": "event", "type": "verify.session.approved", "livemode": false, "created_at": "2026-11-12T10:01:38Z", "data": { "object": { "object": "verify.session", "id": "…", "status": "approved" } } } ``` ## Check the signature [#check-the-signature] Deliveries follow the [Standard Webhooks](https://www.standardwebhooks.com) format, signed with your endpoint's secret (`whsec_…`). Each request carries `webhook-id`, `webhook-timestamp` and `webhook-signature` headers. Check them with any Standard Webhooks library before you trust the payload: ```ts import { Webhook } from "standardwebhooks" const event = new Webhook(process.env.UPSKY_WEBHOOK_SECRET).verify( rawBody, headers ) ``` ## Respond quickly [#respond-quickly] Return a `2xx` within 10 seconds and do slow work in the background. A failed delivery is retried with backoff for up to two days, so the same event can arrive more than once. Use `id` to ignore duplicates. ## Events [#events] | Event | Sent when | | ----------------------------- | ------------------------------------------ | | `verify.session.approved` | A verification passed, or was approved in review | | `verify.session.rejected` | A verification failed, or was rejected in review | | `verify.session.needs_review` | A verification needs a person to decide | | `verify.session.expired` | A link wasn't finished in time | | `verify.session.canceled` | A verification was canceled | | `verify.session.redacted` | A verification's data was deleted | Events carry the session without the data read from the ID. Read the session with the API for that. # Images (/en/ai/images) Image models use OpenAI's Images API, so `images.generate` in any OpenAI SDK works with the same base URL and key as [text](/en/ai). ```ts import { writeFile } from "node:fs/promises" import OpenAI from "openai" const ai = new OpenAI({ apiKey: process.env.UPSKY_API_KEY, baseURL: "https://api.upsky.dev/v1/ai", }) const image = await ai.images.generate({ model: "bfl/flux-2-pro", prompt: "A lighthouse on a cliff at dawn, watercolor", size: "1024x1024", }) await writeFile("lighthouse.png", Buffer.from(image.data[0].b64_json, "base64")) ``` Get the image models from the [models endpoint](/en/ai/reference/listModels) with `?type=image`. ## Options [#options] - `n`: how many images to make, from 1 to 4. Each one is charged. - `size`: width and height in pixels, such as `1024x1024`. Each model supports its own sizes. - `aspect_ratio`: width to height, such as `16:9`, for models that take a ratio instead of a size. - `seed`: makes results repeatable, on models that support it. Images always come back as base64 in `b64_json`, never as links, and Upsky doesn't keep them. Other fields, such as `quality` or `style`, are ignored. ## What it costs [#what-it-costs] Most image models charge a set price per image, listed on the models endpoint as `pricing.image`. Some charge more for larger sizes, listed under `pricing.image_sizes`. A size between two listed ones costs what the next one up does, and a size larger than the largest is refused. A few models, such as OpenAI's, are priced by tokens, like a text model. Upsky sets aside an estimate before the request and charges what the provider reports. You're charged only for the images that come back. `usage.cost` on the reply says what the request cost, in US dollars. ## Test keys [#test-keys] A test key gets a 1×1 transparent PNG for each image asked for. It never calls a model and is never charged. # Overview (/en/ai) Upsky AI lets you call models from every major lab with your team's Upsky API key, the same key every Upsky product uses. It speaks OpenAI's API, so any OpenAI client or SDK works once you change its base URL. Text models use Chat Completions, and there are also [image](/en/ai/images) and [speech](/en/ai/speech) models. ## Connect [#connect] Point your client at `https://api.upsky.dev/v1/ai` and use your [API key](/en/authentication): ```ts import OpenAI from "openai" const ai = new OpenAI({ apiKey: process.env.UPSKY_API_KEY, baseURL: "https://api.upsky.dev/v1/ai", }) const reply = await ai.chat.completions.create({ model: "anthropic/claude-sonnet-5", messages: [{ role: "user", content: "Hello" }], }) console.log(reply.choices[0].message.content) ``` Or with cURL: ```bash curl https://api.upsky.dev/v1/ai/chat/completions \ -H "Authorization: Bearer $UPSKY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-sonnet-5", "messages": [{ "role": "user", "content": "Hello" }] }' ``` ## Choose a model [#choose-a-model] Get the current list from the [models endpoint](/en/ai/reference/listModels). Model IDs always include the provider, as `provider/model-name`. Each model says its `type`, which is `text`, `image` or `speech`, and lists its price: per million tokens for text, per image for images and per million characters for speech. Add `?type=text` to list one type only. ## Streaming [#streaming] Set `"stream": true` to receive the reply as server-sent events while it's written. The last event before `data: [DONE]` reports the tokens used and what the request cost. ## Test keys [#test-keys] A test key gets a sample reply in the same format as a real one, streamed or not, so you can build and test your integration end to end. It never calls a model and is never charged. The model you name still has to exist. ## What it costs [#what-it-costs] Each request is paid from your team's credits at the price on the models list. Live keys need an active Pro plan. - Before a request starts, Upsky sets aside the most it could cost: the prompt plus the longest reply allowed. Only what the request actually used is kept. - Set `max_completion_tokens` to cap a reply. Without it, a team that doesn't have auto top-up on gets a lower limit that its credits cover, rather than being refused. - If your credits don't cover a request and auto top-up can't add more, you get `402` with `insufficient_credits`. - If you close a stream early, you're charged for what the model wrote until then. - `usage.cost` on every reply says what it was charged, in US dollars. ## Limits [#limits] - One choice per request: `n` must be 1. - Function tools only. Built-in tools, such as a provider's web search, are refused. - The default service tier only, with no priority tier or fast mode. - Request bodies up to 4 MB, images and files included. - `Idempotency-Key` is ignored: every request asks the model again. - `user` is dropped. To tell your own users apart, send `safety_identifier`. Errors use the same format as the rest of the API. See [errors](/en/errors). ## Usage [#usage] The **AI** page in the dashboard shows what your team spent this month, day by day, which models it uses most, and its latest requests, text, image and speech alike. # Speech (/en/ai/speech) Speech models use OpenAI's speech API, so `audio.speech.create` in any OpenAI SDK works with the same base URL and key as [text](/en/ai). The reply is the audio file itself. ```ts import { writeFile } from "node:fs/promises" import OpenAI from "openai" const ai = new OpenAI({ apiKey: process.env.UPSKY_API_KEY, baseURL: "https://api.upsky.dev/v1/ai", }) const speech = await ai.audio.speech.create({ model: "openai/tts-1", voice: "alloy", input: "Your order shipped this morning.", }) await writeFile("speech.mp3", Buffer.from(await speech.arrayBuffer())) ``` Get the speech models from the [models endpoint](/en/ai/reference/listModels) with `?type=speech`. ## Options [#options] - `input`: the text to speak, up to 5,000 characters. - `voice`: each provider has its own voices, such as `alloy` for OpenAI or a voice ID for ElevenLabs. Without one, the model uses its default. - `response_format`: `mp3`, `opus`, `aac`, `flac`, `wav` or `pcm`. A model that can't make the format you ask for answers in its own, so check the `Content-Type` header. - `instructions`: how to speak, such as tone or pace, on models that support it. - `speed`: from 0.25 to 4, on models that support it. - `language`: the language of the text, such as `en`, when the model needs it. Upsky doesn't keep the text or the audio. ## What it costs [#what-it-costs] Speech is charged per character of `input`, at the price listed on the models endpoint as `pricing.characters`, per million characters. The price is known before the request, so that's exactly what's charged. The `Upsky-Cost` header on the reply says what it cost, in US dollars, and `Upsky-Characters` how many characters were charged. Music, sound effects and voices priced by the second of audio aren't offered yet. ## Test keys [#test-keys] A test key gets one second of silence, as WAV, whatever format you ask for. It never calls a model and is never charged. # Overview (/en/hosting) Use Upsky Hosting to create sites and deploy them from your own tools. ## Deploy a site [#deploy-a-site] ```bash curl https://api.upsky.dev/v1/hosting/sites/site_123/deployments \ -H "Authorization: Bearer $UPSKY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": { "type": "git", "ref": "main" } }' ``` ## Deployment status [#deployment-status] | Status | Meaning | | ---------- | ------------------------ | | `queued` | Waiting to start | | `building` | Build in progress | | `ready` | Live | | `failed` | The build or launch failed | Poll the deployment, or listen for the `deployment.ready` and `deployment.failed` [webhooks](/en/webhooks). # Authentication (/en/mcp/authentication) The MCP server uses OAuth, so you never paste an API key into an AI tool. 1. Your AI tool opens an Upsky sign-in page. 2. You choose a team and review what the tool is asking for. 3. The tool receives a token that only works for that team. ## Permissions [#permissions] | Scope | Allows | | ------- | ---------------------------------------------- | | `read` | Viewing sites, domains, files and usage | | `write` | Changes such as creating a deployment | Tools that cost money or delete something ask for your confirmation first. ## Remove access [#remove-access] You can revoke a connected tool at any time in your team's settings. It stops working immediately. # Connect a client (/en/mcp/connect) ## Claude Code [#claude-code] ```bash claude mcp add --transport http upsky https://mcp.upsky.dev/mcp ``` ## Claude Desktop [#claude-desktop] Open **Settings**, go to **Connectors**, add a custom connector and enter: ```text https://mcp.upsky.dev/mcp ``` ## Cursor [#cursor] Add this to `~/.cursor/mcp.json`: ```json { "mcpServers": { "upsky": { "url": "https://mcp.upsky.dev/mcp" } } } ``` ## VS Code [#vs-code] Add this to `.vscode/mcp.json`: ```json { "servers": { "upsky": { "type": "http", "url": "https://mcp.upsky.dev/mcp" } } } ``` The first time you use it, your browser opens so you can [sign in](/en/mcp/authentication). # Overview (/en/mcp) The Upsky MCP server lets AI tools like Claude, Cursor and VS Code work with your account using the [Model Context Protocol](https://modelcontextprotocol.io). Once it's connected, you can ask for things like: - "Deploy the main branch of my marketing site." - "Is `example.com` available?" - "How much AI usage did my team have this month?" ## Server URL [#server-url] ```text https://mcp.upsky.dev/mcp ``` The server uses Streamable HTTP, so there's nothing to install. Set it up in your AI tool. How agents get access to your team. Everything an agent can do. # Tools (/en/mcp/tools) ## Verify [#verify] | Tool | Scope | Does | | ---------------------- | ------- | ---------------------------- | | `verify_create_session` | `write` | Starts an identity check | | `verify_get_session` | `read` | Gets a check's status | ## AI [#ai] | Tool | Scope | Does | | ---------------- | ------ | ---------------------- | | `ai_list_models` | `read` | Lists available models | ## Storage [#storage] | Tool | Scope | Does | | ------------------- | ------ | ----------------------------- | | `storage_list_files` | `read` | Lists your team's files | | `storage_get_file` | `read` | Gets a short-lived file link | ## Hosting [#hosting] | Tool | Scope | Does | | -------------------------- | ------- | ------------------------ | | `hosting_list_sites` | `read` | Lists your sites | | `hosting_create_deployment` | `write` | Starts a deployment | | `hosting_get_deployment` | `read` | Gets a deployment's status | ## Platform [#platform] | Tool | Scope | Does | | ------------------------ | ------ | ------------------------- | | `platform_get_team` | `read` | Gets your team's details | | `platform_search_domains` | `read` | Checks domain availability | | `platform_get_usage` | `read` | Gets usage per product | # Overview (/en/platform) The product APIs are what your app calls while it runs. The Platform API is for the account itself. ## What you can manage [#what-you-can-manage] | Resource | What you can do | | -------- | ------------------------ | | Team | Read your team's details | More resources will be added here as they ship. ## Example [#example] A quick way to check that a key works, and which mode it's in: ```bash curl https://api.upsky.dev/v1/platform/team \ -H "Authorization: Bearer $UPSKY_API_KEY" ``` ```json { "id": "0b9f6a52-6d0c-4a3e-9d1e-3f4b2a7c8d90", "name": "Acme", "livemode": false } ``` ## API keys [#api-keys] Keys are created and revoked in your team's dashboard, under **API keys**, by an owner or admin. They can't be created through the API, so a leaked key can't be used to make more keys. Want an AI agent to do this instead? Use the [MCP server](/en/mcp). # Overview (/en/storage) Upsky Storage gives your apps private buckets that speak the S3 API, so any S3 client or SDK works with them. Downloads are always free. ## Get started [#get-started] 1. In the dashboard, open **Storage** and create a bucket. Every team gets one 10 GB bucket for free. 2. Open the bucket and create an **access key**. Copy the Access Key ID and the Secret Access Key: the secret is shown only once. 3. Click **Connect** on the Storage page to copy the endpoint. The region is always `auto`. Each access key reaches one bucket only. Make one per app or environment, so you can delete one without breaking the others. ## Connect a client [#connect-a-client] ```ts import { S3Client } from "@aws-sdk/client-s3" const storage = new S3Client({ region: "auto", endpoint: process.env.UPSKY_STORAGE_ENDPOINT, credentials: { accessKeyId: process.env.UPSKY_STORAGE_ACCESS_KEY_ID, secretAccessKey: process.env.UPSKY_STORAGE_SECRET_ACCESS_KEY, }, }) ``` Use the bucket name shown on the bucket's page, which starts with `upsky-`, as `Bucket` in every request. ## Upload a file [#upload-a-file] ```ts import { PutObjectCommand } from "@aws-sdk/client-s3" await storage.send( new PutObjectCommand({ Bucket: process.env.UPSKY_STORAGE_BUCKET, Key: "reports/2026/report.pdf", Body: file, ContentType: "application/pdf", }) ) ``` A `/` in the key works like a folder, and the dashboard's file browser shows it as one. ## Share a file [#share-a-file] Buckets are private. To let a browser download or upload a file directly, sign a short-lived URL on your server: ```ts import { GetObjectCommand } from "@aws-sdk/client-s3" import { getSignedUrl } from "@aws-sdk/s3-request-presigner" const url = await getSignedUrl( storage, new GetObjectCommand({ Bucket: process.env.UPSKY_STORAGE_BUCKET, Key: "reports/2026/report.pdf", }), { expiresIn: 300 } ) ``` ## Sizes and usage [#sizes-and-usage] Each size includes a number of writes and reads every month. Writes are requests that change or list the bucket, such as uploads; reads are requests that fetch, such as downloads. Deletes are free. Writes and reads beyond the allowance are paid from your credits. If they can't be paid, new uploads pause until you add credits, and reads keep working. A full bucket also pauses uploads until you delete files or move to a larger size. See [pricing](https://upsky.dev/en/pricing) for the current sizes and prices. # Overview (/en/verify) Use Upsky Verify to check a person's identity before they use your app. It has [verification tiers](/en/verify/tiers), from a selfie with an age estimate to an official ID matched to the person's face. ## How it works [#how-it-works] 1. Your server creates a session. 2. You send the person to the session's `url`, a page on `verify.upsky.dev` that works on any phone. You can also start one from the dashboard, which gives you a QR code and a link to share. 3. They agree to the notice, photograph their ID if the tier needs it, and take a selfie. 4. Upsky runs the checks and sends a [webhook](/en/webhooks) with the result. You can also read the session at any time. You're charged only when a session reaches a decision. A link that expires or that you cancel costs nothing. ## Create a session [#create-a-session] ```bash curl https://api.upsky.dev/v1/verify/sessions \ -H "Authorization: Bearer $UPSKY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "tier": "identity", "reference": "user_123", "return_url": "https://example.com/done" }' ``` | Field | Default | What it does | | --------------------- | ---------- | -------------------------------------------------- | | `tier` | `identity` | `age` or `identity` | | `reference` | | Your own ID for the person | | `return_url` | | Where the person goes after submitting | | `minimum_age` | `18` | From 13 to 25 | | `allow_selfie_upload` | `false` | Let the person send a selfie from their photos | | `show_result` | `false` | Show the person the result on the link | | `expires_in` | `86400` | Seconds the link works, from an hour to seven days | | `project_id` | | One of your projects, to group sessions | | `metadata` | | Up to 20 keys of your own | | `options` | | Accepted IDs, outcome rules and photo retention | | `expected` | | What you expect the ID to say | ## Selfies [#selfies] By default the selfie has to be taken with the camera on the page, there and then. Set `allow_selfie_upload` to `true` to also let the person choose a photo they already have. The session returns the setting as `allow_selfie_upload`, and the dashboard has the same switch when you start a verification. Live sessions always take the selfie with the camera, through the liveness check, whatever this is set to. ## Showing the result [#showing-the-result] By default the link only thanks the person, and you tell them the result yourself. Set `show_result` to `true` to show it on the link instead: right after they send their photos, the page waits for the checks and says whether they were verified, weren't, or are being reviewed. It never says why, so the page can't be used to learn what gets past the checks. The dashboard has the same switch when you start a verification. If a session in review is later approved or rejected, the link shows the new result the next time it's opened. ## Approval thresholds [#approval-thresholds] How sure each check must be is your team's choice, in the dashboard under **Verify → Settings**. Pick a preset, or set each number yourself with Custom: | Preset | Liveness pass | Liveness review | Face match pass | Face match review | Age margin | | -------- | ------------- | --------------- | --------------- | ----------------- | ---------- | | Relaxed | 65 | 40 | 90 | 80 | 2 years | | Balanced | 80 | 50 | 95 | 85 | 3 years | | Strict | 90 | 70 | 98 | 92 | 5 years | A score at or above a pass mark passes. From the review mark up to the pass mark, the session goes to review (`liveness_inconclusive` or `face_match_inconclusive`), so a person can look. Below the review mark it's rejected (`liveness_failed` or `face_mismatch`). On the `age` tier, a selfie alone passes when the person looks at least `minimum_age` plus the age margin: the middle of the estimated range, returned as `checks.age.estimated_age`. For an 18+ check, that's from looking 20 on Relaxed, 21 on Balanced and 23 on Strict. A range entirely under `minimum_age` is rejected, and anything between goes to review. Balanced is the default. A project can have its own thresholds, chosen in the same place: a session with that `project_id` uses them, and every other session uses your team's. Each session keeps the thresholds it was created with, returned as `thresholds`, so changing your settings never changes how an existing session is decided. When a liveness check scores under the pass mark, the page suggests trying again, within the link's three tries. ## Accepted IDs [#accepted-ids] On the `identity` tier, you choose which IDs a person may use and which countries they may come from, in the dashboard under **Verify → Settings** or per session: ```bash curl https://api.upsky.dev/v1/verify/sessions \ -H "Authorization: Bearer $UPSKY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "options": { "documents": { "allowed_types": ["passport", "mx_ine"], "allowed_countries": ["MEX", "USA"] } } }' ``` `allowed_types` is any of `mx_ine`, `passport`, `us_drivers_license` and `id_card`. The person only sees the ones you accept, and isn't asked to choose when there's just one. `allowed_countries` takes ISO 3166-1 alpha-3 codes; leave it empty to accept any country. An INE only counts as `MEX` and a US driver's license as `USA`, so a request that accepts neither a matching country nor another ID is refused. The ID is read as soon as its photos are in. If it's blurry, isn't the kind the person chose, has expired or comes from a country you don't accept, the page says so and asks for new photos, up to `max_attempts` times (3 by default, 1 to 5). After the last try the person sends the photos they have, and your outcome rules decide. ## Outcome rules [#outcome-rules] Each problem a check can find leads to one of three outcomes: `reject` the session, send it to `review`, or `ignore` it. Set them in **Verify → Settings**, or per session in `options.rules`: | Rule | Default | Can be | | ------------------------------- | -------- | ---------------------------- | | `liveness_inconclusive` | `review` | `reject`, `review` | | `age_inconclusive` | `review` | `reject`, `review` | | `document_unreadable` | `reject` | `reject`, `review` | | `document_expired` | `reject` | `reject`, `review`, `ignore` | | `document_checksum_failed` | `reject` | `reject`, `review` | | `document_type_mismatch` | `review` | `reject`, `review` | | `document_country_not_accepted` | `reject` | `reject`, `review` | | `date_of_birth_missing` | `review` | `reject`, `review` | | `face_match_inconclusive` | `review` | `reject`, `review` | | `age_estimate_mismatch` | `review` | `reject`, `review`, `ignore` | | `name_mismatch` | `review` | `reject`, `review`, `ignore` | | `date_of_birth_mismatch` | `review` | `reject`, `review`, `ignore` | A failed liveness check, a selfie with no face, a face that isn't the ID's and an age under the minimum always reject. ## Expected data [#expected-data] If you already know who the person should be, send it as `expected` and the ID is compared with it: ```json { "tier": "identity", "expected": { "first_name": "Ana", "last_name": "García", "date_of_birth": "1992-04-15" } } ``` Names match whatever their accents, case or middle names: "Ana" matches "ANA MARIA". The result is in `checks.data_match`, field by field: `match`, `mismatch`, or `not_read` when the ID gave nothing to compare. A difference sends the session to review unless you change the `name_mismatch` or `date_of_birth_mismatch` rule. ## Branding [#branding] The page a link opens can carry your name, your logo, your color and a short message of your own. Set them in **Verify → Settings**, for your team or for a project; a session with that `project_id` uses the project's. Changes reach links you've already sent. ## Data retention and redaction [#data-retention-and-redaction] Photos are deleted 30 days after a session ends. Change that in **Verify → Settings**, or per session with `options.retention_days` (1 to 365). A session waiting for your review keeps its photos at least 7 days. To delete one person's data sooner, redact the session: ```bash curl -X POST https://api.upsky.dev/v1/verify/sessions/$SESSION_ID/redact \ -H "Authorization: Bearer $UPSKY_API_KEY" ``` This deletes the photos, what was read from the ID, the `expected` data, and your `reference`, `metadata` and `return_url`, right away. The status, reasons and checks stay as your record, and `redacted_at` says when it happened. It can't be undone, only works on a session that has ended, and sends a `verify.session.redacted` webhook. Every setting above follows the same order: what the request sets, then the project's settings, then your team's. Each session keeps the options it was created with, returned as `options`. ## Session status [#session-status] | Status | Meaning | | -------------- | ----------------------------------------------------------------- | | `pending` | The link hasn't been opened | | `in_progress` | The person is taking their photos | | `processing` | Submitted; the checks are running | | `approved` | Every check passed | | `rejected` | A check failed. `reasons` says which | | `needs_review` | The checks couldn't decide. Approve or reject it in the dashboard | | `expired` | The link wasn't finished in time. Not charged | | `canceled` | You canceled it. Not charged | A decided session has `checks` (liveness, age, document, face match and, when you sent `expected`, data match, with their scores) and, for the identity tier, `verified_outputs`: the name, date of birth and document details read from the ID. ## Test mode [#test-mode] Sessions made with a test key run the whole flow with simulated checks and are never charged. On the last step, the person picks the result they want, so you can test approved, rejected and needs-review without real IDs. ## Live mode [#live-mode] Sessions made with a live key run real checks and are charged when they reach a decision. Live sessions are open for the `age` and `identity` tiers. The person gets three tries at the liveness check on each link. After the third, the link can't continue: create a new session for them. A link that runs out of tries and expires isn't charged. # Verification tiers (/en/verify/tiers) Upsky Verify has three tiers. Each one checks more than the one before it. ## Tier 1: Age & Liveness [#tier-1-age--liveness] Age estimation and proof of presence. - The user takes a selfie with a liveness check. - AI estimates their age range. - Returns a risk result for the 18-year threshold. - Cases close to the threshold require ID. Doesn't prove the person's exact age or legal identity. ## Tier 2: Identity [#tier-2-identity] Everything in Tier 1, plus official ID. - ID capture, front and back. - Name and date of birth extraction. - Document check that the person is of legal age. - Selfie with a liveness check. - Face match between the selfie and the ID. Verifies the biometric match and the data read, but doesn't confirm the ID was actually issued by the government. ## Tier 3: Identity Plus [#tier-3-identity-plus] Enhanced document verification and authorized lookup. - Everything in Tier 2. - Specialized document authenticity analysis. - ID validity check through an authorized provider. - Detailed results of every check run. The validity check isn't necessarily a biometric match against official records. Its scope depends on the provider and the contract. # List sessions (/en/verify/reference/listVerifySessions) { "openapi": "3.2.0", "info": { "title": "Upsky Verify API", "version": "1.0.0" }, "servers": [ { "url": "https://api.upsky.dev" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "Your API key, starting with upsky_test_ or upsky_live_." } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "invalid_request", "unauthorized", "insufficient_credits", "pro_required", "account_suspended", "forbidden", "not_found", "method_not_allowed", "conflict", "idempotency_key_reused", "request_in_progress", "retry_later", "payload_too_large", "unsupported_media_type", "rate_limited", "internal_error", "service_unavailable" ], "description": "A stable code to branch on.", "example": "unauthorized" }, "message": { "type": "string", "description": "What went wrong, for a person to read.", "example": "The API key is not valid or has been revoked." }, "request_id": { "type": "string", "description": "Include this when you contact support.", "example": "req_3f9a1c0b7d2e4a6f8b1c2d3e" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] }, "VerifySession": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "object": { "type": "string", "enum": [ "verify.session" ] }, "livemode": { "type": "boolean" }, "tier": { "type": "string", "enum": [ "age", "identity", "identity_plus" ] }, "status": { "type": "string", "enum": [ "pending", "in_progress", "processing", "approved", "rejected", "needs_review", "expired", "canceled" ], "description": "`pending` until the person opens the link, `in_progress` while they take photos, `processing` once submitted, then `approved`, `rejected` or `needs_review`. A link that's never finished ends `expired` or `canceled`, and isn't charged." }, "url": { "type": [ "string", "null" ], "description": "The link to send the person to. Null once it can't be used anymore.", "example": "https://verify.upsky.dev/s/9Qm2..." }, "reference": { "type": [ "string", "null" ] }, "metadata": { "type": "object", "additionalProperties": { "type": "string" } }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "return_url": { "type": [ "string", "null" ] }, "minimum_age": { "type": "integer" }, "allow_selfie_upload": { "type": "boolean", "description": "Whether the person may send a selfie from their photos. When false, it has to be taken with the camera on the page." }, "show_result": { "type": "boolean", "description": "Whether the link shows the person the result once it's decided." }, "thresholds": { "type": "object", "properties": { "preset": { "type": "string", "enum": [ "relaxed", "balanced", "strict", "custom" ] }, "liveness_pass": { "type": "number" }, "liveness_review": { "type": "number" }, "face_match_pass": { "type": "number" }, "face_match_review": { "type": "number" }, "age_buffer_years": { "type": "integer" } }, "required": [ "preset", "liveness_pass", "liveness_review", "face_match_pass", "face_match_review", "age_buffer_years" ], "description": "How sure each check had to be, copied when the session was created from its project's Verify settings, or your team's if the project has none. A score at or above a pass mark passes; from its review mark up to the pass mark goes to review; below the review mark is rejected." }, "options": { "$ref": "#/components/schemas/VerifySessionOptions" }, "reasons": { "type": "array", "items": { "type": "string", "enum": [ "liveness_failed", "liveness_inconclusive", "face_not_found", "age_under_minimum", "age_inconclusive", "document_unreadable", "document_expired", "document_checksum_failed", "document_type_mismatch", "document_country_not_accepted", "date_of_birth_missing", "face_mismatch", "face_match_inconclusive", "age_estimate_mismatch", "name_mismatch", "date_of_birth_mismatch", "processing_error", "reviewed" ] }, "description": "Why the session ended as it did.", "example": [ "face_mismatch" ] }, "checks": { "$ref": "#/components/schemas/VerifyChecks" }, "document_type": { "type": [ "string", "null" ], "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card", null ] }, "verified_outputs": { "$ref": "#/components/schemas/VerifiedOutputs" }, "expires_at": { "type": "string" }, "created_at": { "type": "string" }, "started_at": { "type": [ "string", "null" ] }, "submitted_at": { "type": [ "string", "null" ] }, "decided_at": { "type": [ "string", "null" ] }, "reviewed_at": { "type": [ "string", "null" ] }, "redacted_at": { "type": [ "string", "null" ], "description": "When the person's data was deleted from the session with the redact endpoint." } }, "required": [ "id", "object", "livemode", "tier", "status", "url", "reference", "metadata", "project_id", "return_url", "minimum_age", "allow_selfie_upload", "show_result", "thresholds", "options", "reasons", "checks", "document_type", "verified_outputs", "expires_at", "created_at", "started_at", "submitted_at", "decided_at", "reviewed_at", "redacted_at" ] }, "VerifySessionOptions": { "type": "object", "properties": { "documents": { "type": "object", "properties": { "allowed_types": { "type": "array", "items": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "description": "The IDs the person may use." }, "allowed_countries": { "type": "array", "items": { "type": "string" }, "description": "Issuing countries accepted, as ISO 3166-1 alpha-3 codes. Empty means any country.", "example": [ "MEX", "USA" ] }, "max_attempts": { "type": "integer", "description": "How many times the ID's photos are read while the person is on the page. A read that finds a blurry photo, the wrong kind of ID, an expired one or a country you don't accept asks them to try again, until these run out." } }, "required": [ "allowed_types", "allowed_countries", "max_attempts" ] }, "rules": { "$ref": "#/components/schemas/VerifyRules" }, "retention_days": { "type": "integer", "description": "Days after the session ends when its photos are deleted. A session waiting for review keeps them at least 7 days." } }, "required": [ "documents", "rules", "retention_days" ], "description": "What the session runs by, copied when it was created from its project's Verify settings, or your team's, with anything you set in the request on top." }, "VerifyRules": { "type": "object", "properties": { "liveness_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_unreadable": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_expired": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `reject` unless you change it." }, "document_checksum_failed": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_type_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_country_not_accepted": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "date_of_birth_missing": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "face_match_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_estimate_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "name_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "date_of_birth_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." } }, "required": [ "liveness_inconclusive", "age_inconclusive", "document_unreadable", "document_expired", "document_checksum_failed", "document_type_mismatch", "document_country_not_accepted", "date_of_birth_missing", "face_match_inconclusive", "age_estimate_mismatch", "name_mismatch", "date_of_birth_mismatch" ], "description": "What each problem leads to: `reject` the session, send it to `review`, or `ignore` it. Problems that aren't here always reject: a failed liveness check, no face, a face that isn't the ID's, or an age under the minimum." }, "VerifyChecks": { "type": "object", "properties": { "liveness": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "confidence": { "type": [ "number", "null" ], "description": "AWS Face Liveness confidence, 0 to 100." }, "feedback": { "type": "array", "items": { "type": "string", "enum": [ "face_not_visible", "face_obstructed", "face_not_aligned", "eyes_closed", "low_lighting", "high_lighting", "low_video_quality" ] }, "description": "What lowered the liveness score, such as `high_lighting`. Empty when nothing did." } }, "required": [ "status", "confidence", "feedback" ] }, "age": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "minimum_age": { "type": "integer" }, "estimated_low": { "type": [ "number", "null" ] }, "estimated_high": { "type": [ "number", "null" ] }, "estimated_age": { "type": [ "number", "null" ], "description": "The age the person looks: the middle of the estimated range. On the `age` tier it passes from `minimum_age` plus your age margin." }, "age_from_document": { "type": [ "integer", "null" ] } }, "required": [ "status", "minimum_age", "estimated_low", "estimated_high", "estimated_age", "age_from_document" ] }, "document": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "document_type": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "expired": { "type": [ "boolean", "null" ] }, "checksums_valid": { "type": [ "boolean", "null" ] }, "type_matches": { "type": [ "boolean", "null" ], "description": "Whether the ID is the kind the person chose. Null when the read couldn't tell." }, "country_accepted": { "type": [ "boolean", "null" ], "description": "Whether the ID's country is one the session accepts. Null when any country is, or the country couldn't be read." } }, "required": [ "status", "document_type", "expired", "checksums_valid", "type_matches", "country_accepted" ] }, "face_match": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "similarity": { "type": [ "number", "null" ], "description": "How alike the selfie and the ID's photo are, 0 to 100." } }, "required": [ "status", "similarity" ] }, "data_match": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "first_name": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] }, "last_name": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] }, "date_of_birth": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] } }, "required": [ "status", "first_name", "last_name", "date_of_birth" ], "description": "How the ID compared with the `expected` data you sent. Only on sessions created with it. A field is null when you expected nothing for it, and `not_read` when the ID gave nothing to compare." } }, "description": "What each check found. Empty until the session is decided." }, "VerifiedOutputs": { "type": [ "object", "null" ], "properties": { "first_name": { "type": [ "string", "null" ] }, "last_name": { "type": [ "string", "null" ] }, "date_of_birth": { "type": [ "string", "null" ], "example": "1992-04-15" }, "document_type": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "document_number": { "type": [ "string", "null" ] }, "issuing_country": { "type": [ "string", "null" ], "example": "MEX" }, "expires_on": { "type": [ "string", "null" ], "example": "2031-12-31" } }, "required": [ "first_name", "last_name", "date_of_birth", "document_type", "document_number", "issuing_country", "expires_on" ], "description": "What was read from the ID. Identity tier and above, once decided. Only returned when you read one session, never in lists or webhooks." }, "CreateVerifySession": { "type": "object", "properties": { "tier": { "type": "string", "enum": [ "age", "identity", "identity_plus" ], "default": "identity", "description": "`age`: selfie with liveness and an age estimate. `identity`: adds an ID, front and back, and a face match. `identity_plus` isn't available yet." }, "reference": { "type": "string", "minLength": 1, "maxLength": 200, "description": "Your own ID for this person. Only your team sees it.", "example": "user_123" }, "return_url": { "type": "string", "maxLength": 2000, "description": "Where to send the person once they're done: a web page, or your app through its own scheme (`myapp://verified`) to bring them back from an in-app browser. The page goes there by itself a few seconds after they submit, or after the result when `show_result` is on, and also shows a button.", "example": "https://example.com/verified" }, "minimum_age": { "type": "integer", "minimum": 13, "maximum": 25, "default": 18 }, "allow_selfie_upload": { "type": "boolean", "default": false, "description": "Let the person send a selfie from their photos instead of taking it with the camera. Camera only by default. Live sessions always take the selfie with the camera, through the liveness check." }, "show_result": { "type": "boolean", "default": false, "description": "Show the person the result on the link right after they send it: approved, rejected or in review, never why. Off by default, so you tell them yourself." }, "expires_in": { "type": "integer", "minimum": 3600, "maximum": 604800, "default": 86400, "description": "Seconds the link works for, from an hour to seven days. A day by default." }, "project_id": { "type": "string", "format": "uuid", "description": "One of your team's projects, to group sessions by. The session uses the project's Verify settings where it has its own." }, "options": { "$ref": "#/components/schemas/CreateVerifySessionOptions" }, "expected": { "$ref": "#/components/schemas/VerifyExpectedData" }, "metadata": { "type": "object", "additionalProperties": { "type": "string", "maxLength": 500 }, "description": "Up to 20 keys of your own, returned as given." } } }, "CreateVerifySessionOptions": { "type": "object", "properties": { "documents": { "type": "object", "properties": { "allowed_types": { "type": "array", "items": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "minItems": 1, "description": "The IDs the person may use." }, "allowed_countries": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z]{3}$", "example": "MEX" }, "maxItems": 250, "description": "Issuing countries accepted, as ISO 3166-1 alpha-3 codes. Empty means any country." }, "max_attempts": { "type": "integer", "minimum": 1, "maximum": 5, "description": "How many times the ID's photos are read while the person is on the page, 1 to 5." } }, "additionalProperties": false }, "rules": { "$ref": "#/components/schemas/VerifyRuleOverrides" }, "retention_days": { "type": "integer", "minimum": 1, "maximum": 365, "description": "Days after the session ends when its photos are deleted, 1 to 365." } }, "additionalProperties": false, "description": "Anything this session should do differently from its project's Verify settings, or your team's. Leave a key out to keep the setting." }, "VerifyRuleOverrides": { "type": "object", "properties": { "liveness_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_unreadable": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_expired": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `reject` unless you change it." }, "document_checksum_failed": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_type_mismatch": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_country_not_accepted": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "date_of_birth_missing": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "face_match_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_estimate_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "name_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "date_of_birth_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." } }, "additionalProperties": false }, "VerifyExpectedData": { "type": "object", "properties": { "first_name": { "type": "string", "minLength": 1, "maxLength": 100 }, "last_name": { "type": "string", "minLength": 1, "maxLength": 100 }, "date_of_birth": { "type": "string", "format": "date", "example": "1992-04-15" } }, "additionalProperties": false, "description": "What you expect the ID to say. Names match whatever their accents, case or middle names. `identity` tier only. The result is in `checks.data_match`; by default a difference sends the session to review." }, "VerifySessionList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ "list" ] }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/VerifySession" } }, "has_more": { "type": "boolean" } }, "required": [ "object", "data", "has_more" ] } }, "parameters": {} }, "paths": { "/v1/verify/sessions": { "post": { "operationId": "createVerifySession", "tags": [ "Verify" ], "summary": "Create a session", "description": "Starts a verification and returns the `url` to send the person to. A live key sets the tier's price aside, which is kept only if the session reaches a decision.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateVerifySession" } } } }, "responses": { "201": { "description": "The new session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "400": { "description": "Something in the request isn't valid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need Upsky AI Pro.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "Live checks aren't connected right now, or prices couldn't be read.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "get": { "operationId": "listVerifySessions", "tags": [ "Verify" ], "summary": "List sessions", "description": "Your team's sessions in the key's mode, newest first. Pass the last `id` as `starting_after` for the next page.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "required": false, "name": "limit", "in": "query" }, { "schema": { "type": "string", "format": "uuid" }, "required": false, "name": "starting_after", "in": "query" }, { "schema": { "type": "string", "enum": [ "pending", "in_progress", "processing", "approved", "rejected", "needs_review", "expired", "canceled" ] }, "required": false, "name": "status", "in": "query" } ], "responses": { "200": { "description": "The sessions.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySessionList" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}": { "get": { "operationId": "getVerifySession", "tags": [ "Verify" ], "summary": "Get a session", "description": "Returns a session, with what was read from the ID once it's decided.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}/cancel": { "post": { "operationId": "cancelVerifySession", "tags": [ "Verify" ], "summary": "Cancel a session", "description": "Stops a session the person hasn't submitted. Its link stops working and nothing is charged.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The canceled session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The session was already submitted or ended.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}/redact": { "post": { "operationId": "redactVerifySession", "tags": [ "Verify" ], "summary": "Redact a session", "description": "Deletes everything about the person from a session that has ended, right away: the photos, what was read from the ID, the `expected` data, and your `reference`, `metadata` and `return_url`. The status, reasons and checks stay as your record. It can't be undone. Redacting a session twice changes nothing.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The redacted session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The session hasn't ended. Cancel it first, or wait for its result.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": {} } # Create a session (/en/verify/reference/createVerifySession) { "openapi": "3.2.0", "info": { "title": "Upsky Verify API", "version": "1.0.0" }, "servers": [ { "url": "https://api.upsky.dev" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "Your API key, starting with upsky_test_ or upsky_live_." } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "invalid_request", "unauthorized", "insufficient_credits", "pro_required", "account_suspended", "forbidden", "not_found", "method_not_allowed", "conflict", "idempotency_key_reused", "request_in_progress", "retry_later", "payload_too_large", "unsupported_media_type", "rate_limited", "internal_error", "service_unavailable" ], "description": "A stable code to branch on.", "example": "unauthorized" }, "message": { "type": "string", "description": "What went wrong, for a person to read.", "example": "The API key is not valid or has been revoked." }, "request_id": { "type": "string", "description": "Include this when you contact support.", "example": "req_3f9a1c0b7d2e4a6f8b1c2d3e" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] }, "VerifySession": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "object": { "type": "string", "enum": [ "verify.session" ] }, "livemode": { "type": "boolean" }, "tier": { "type": "string", "enum": [ "age", "identity", "identity_plus" ] }, "status": { "type": "string", "enum": [ "pending", "in_progress", "processing", "approved", "rejected", "needs_review", "expired", "canceled" ], "description": "`pending` until the person opens the link, `in_progress` while they take photos, `processing` once submitted, then `approved`, `rejected` or `needs_review`. A link that's never finished ends `expired` or `canceled`, and isn't charged." }, "url": { "type": [ "string", "null" ], "description": "The link to send the person to. Null once it can't be used anymore.", "example": "https://verify.upsky.dev/s/9Qm2..." }, "reference": { "type": [ "string", "null" ] }, "metadata": { "type": "object", "additionalProperties": { "type": "string" } }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "return_url": { "type": [ "string", "null" ] }, "minimum_age": { "type": "integer" }, "allow_selfie_upload": { "type": "boolean", "description": "Whether the person may send a selfie from their photos. When false, it has to be taken with the camera on the page." }, "show_result": { "type": "boolean", "description": "Whether the link shows the person the result once it's decided." }, "thresholds": { "type": "object", "properties": { "preset": { "type": "string", "enum": [ "relaxed", "balanced", "strict", "custom" ] }, "liveness_pass": { "type": "number" }, "liveness_review": { "type": "number" }, "face_match_pass": { "type": "number" }, "face_match_review": { "type": "number" }, "age_buffer_years": { "type": "integer" } }, "required": [ "preset", "liveness_pass", "liveness_review", "face_match_pass", "face_match_review", "age_buffer_years" ], "description": "How sure each check had to be, copied when the session was created from its project's Verify settings, or your team's if the project has none. A score at or above a pass mark passes; from its review mark up to the pass mark goes to review; below the review mark is rejected." }, "options": { "$ref": "#/components/schemas/VerifySessionOptions" }, "reasons": { "type": "array", "items": { "type": "string", "enum": [ "liveness_failed", "liveness_inconclusive", "face_not_found", "age_under_minimum", "age_inconclusive", "document_unreadable", "document_expired", "document_checksum_failed", "document_type_mismatch", "document_country_not_accepted", "date_of_birth_missing", "face_mismatch", "face_match_inconclusive", "age_estimate_mismatch", "name_mismatch", "date_of_birth_mismatch", "processing_error", "reviewed" ] }, "description": "Why the session ended as it did.", "example": [ "face_mismatch" ] }, "checks": { "$ref": "#/components/schemas/VerifyChecks" }, "document_type": { "type": [ "string", "null" ], "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card", null ] }, "verified_outputs": { "$ref": "#/components/schemas/VerifiedOutputs" }, "expires_at": { "type": "string" }, "created_at": { "type": "string" }, "started_at": { "type": [ "string", "null" ] }, "submitted_at": { "type": [ "string", "null" ] }, "decided_at": { "type": [ "string", "null" ] }, "reviewed_at": { "type": [ "string", "null" ] }, "redacted_at": { "type": [ "string", "null" ], "description": "When the person's data was deleted from the session with the redact endpoint." } }, "required": [ "id", "object", "livemode", "tier", "status", "url", "reference", "metadata", "project_id", "return_url", "minimum_age", "allow_selfie_upload", "show_result", "thresholds", "options", "reasons", "checks", "document_type", "verified_outputs", "expires_at", "created_at", "started_at", "submitted_at", "decided_at", "reviewed_at", "redacted_at" ] }, "VerifySessionOptions": { "type": "object", "properties": { "documents": { "type": "object", "properties": { "allowed_types": { "type": "array", "items": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "description": "The IDs the person may use." }, "allowed_countries": { "type": "array", "items": { "type": "string" }, "description": "Issuing countries accepted, as ISO 3166-1 alpha-3 codes. Empty means any country.", "example": [ "MEX", "USA" ] }, "max_attempts": { "type": "integer", "description": "How many times the ID's photos are read while the person is on the page. A read that finds a blurry photo, the wrong kind of ID, an expired one or a country you don't accept asks them to try again, until these run out." } }, "required": [ "allowed_types", "allowed_countries", "max_attempts" ] }, "rules": { "$ref": "#/components/schemas/VerifyRules" }, "retention_days": { "type": "integer", "description": "Days after the session ends when its photos are deleted. A session waiting for review keeps them at least 7 days." } }, "required": [ "documents", "rules", "retention_days" ], "description": "What the session runs by, copied when it was created from its project's Verify settings, or your team's, with anything you set in the request on top." }, "VerifyRules": { "type": "object", "properties": { "liveness_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_unreadable": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_expired": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `reject` unless you change it." }, "document_checksum_failed": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_type_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_country_not_accepted": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "date_of_birth_missing": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "face_match_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_estimate_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "name_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "date_of_birth_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." } }, "required": [ "liveness_inconclusive", "age_inconclusive", "document_unreadable", "document_expired", "document_checksum_failed", "document_type_mismatch", "document_country_not_accepted", "date_of_birth_missing", "face_match_inconclusive", "age_estimate_mismatch", "name_mismatch", "date_of_birth_mismatch" ], "description": "What each problem leads to: `reject` the session, send it to `review`, or `ignore` it. Problems that aren't here always reject: a failed liveness check, no face, a face that isn't the ID's, or an age under the minimum." }, "VerifyChecks": { "type": "object", "properties": { "liveness": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "confidence": { "type": [ "number", "null" ], "description": "AWS Face Liveness confidence, 0 to 100." }, "feedback": { "type": "array", "items": { "type": "string", "enum": [ "face_not_visible", "face_obstructed", "face_not_aligned", "eyes_closed", "low_lighting", "high_lighting", "low_video_quality" ] }, "description": "What lowered the liveness score, such as `high_lighting`. Empty when nothing did." } }, "required": [ "status", "confidence", "feedback" ] }, "age": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "minimum_age": { "type": "integer" }, "estimated_low": { "type": [ "number", "null" ] }, "estimated_high": { "type": [ "number", "null" ] }, "estimated_age": { "type": [ "number", "null" ], "description": "The age the person looks: the middle of the estimated range. On the `age` tier it passes from `minimum_age` plus your age margin." }, "age_from_document": { "type": [ "integer", "null" ] } }, "required": [ "status", "minimum_age", "estimated_low", "estimated_high", "estimated_age", "age_from_document" ] }, "document": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "document_type": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "expired": { "type": [ "boolean", "null" ] }, "checksums_valid": { "type": [ "boolean", "null" ] }, "type_matches": { "type": [ "boolean", "null" ], "description": "Whether the ID is the kind the person chose. Null when the read couldn't tell." }, "country_accepted": { "type": [ "boolean", "null" ], "description": "Whether the ID's country is one the session accepts. Null when any country is, or the country couldn't be read." } }, "required": [ "status", "document_type", "expired", "checksums_valid", "type_matches", "country_accepted" ] }, "face_match": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "similarity": { "type": [ "number", "null" ], "description": "How alike the selfie and the ID's photo are, 0 to 100." } }, "required": [ "status", "similarity" ] }, "data_match": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "first_name": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] }, "last_name": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] }, "date_of_birth": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] } }, "required": [ "status", "first_name", "last_name", "date_of_birth" ], "description": "How the ID compared with the `expected` data you sent. Only on sessions created with it. A field is null when you expected nothing for it, and `not_read` when the ID gave nothing to compare." } }, "description": "What each check found. Empty until the session is decided." }, "VerifiedOutputs": { "type": [ "object", "null" ], "properties": { "first_name": { "type": [ "string", "null" ] }, "last_name": { "type": [ "string", "null" ] }, "date_of_birth": { "type": [ "string", "null" ], "example": "1992-04-15" }, "document_type": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "document_number": { "type": [ "string", "null" ] }, "issuing_country": { "type": [ "string", "null" ], "example": "MEX" }, "expires_on": { "type": [ "string", "null" ], "example": "2031-12-31" } }, "required": [ "first_name", "last_name", "date_of_birth", "document_type", "document_number", "issuing_country", "expires_on" ], "description": "What was read from the ID. Identity tier and above, once decided. Only returned when you read one session, never in lists or webhooks." }, "CreateVerifySession": { "type": "object", "properties": { "tier": { "type": "string", "enum": [ "age", "identity", "identity_plus" ], "default": "identity", "description": "`age`: selfie with liveness and an age estimate. `identity`: adds an ID, front and back, and a face match. `identity_plus` isn't available yet." }, "reference": { "type": "string", "minLength": 1, "maxLength": 200, "description": "Your own ID for this person. Only your team sees it.", "example": "user_123" }, "return_url": { "type": "string", "maxLength": 2000, "description": "Where to send the person once they're done: a web page, or your app through its own scheme (`myapp://verified`) to bring them back from an in-app browser. The page goes there by itself a few seconds after they submit, or after the result when `show_result` is on, and also shows a button.", "example": "https://example.com/verified" }, "minimum_age": { "type": "integer", "minimum": 13, "maximum": 25, "default": 18 }, "allow_selfie_upload": { "type": "boolean", "default": false, "description": "Let the person send a selfie from their photos instead of taking it with the camera. Camera only by default. Live sessions always take the selfie with the camera, through the liveness check." }, "show_result": { "type": "boolean", "default": false, "description": "Show the person the result on the link right after they send it: approved, rejected or in review, never why. Off by default, so you tell them yourself." }, "expires_in": { "type": "integer", "minimum": 3600, "maximum": 604800, "default": 86400, "description": "Seconds the link works for, from an hour to seven days. A day by default." }, "project_id": { "type": "string", "format": "uuid", "description": "One of your team's projects, to group sessions by. The session uses the project's Verify settings where it has its own." }, "options": { "$ref": "#/components/schemas/CreateVerifySessionOptions" }, "expected": { "$ref": "#/components/schemas/VerifyExpectedData" }, "metadata": { "type": "object", "additionalProperties": { "type": "string", "maxLength": 500 }, "description": "Up to 20 keys of your own, returned as given." } } }, "CreateVerifySessionOptions": { "type": "object", "properties": { "documents": { "type": "object", "properties": { "allowed_types": { "type": "array", "items": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "minItems": 1, "description": "The IDs the person may use." }, "allowed_countries": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z]{3}$", "example": "MEX" }, "maxItems": 250, "description": "Issuing countries accepted, as ISO 3166-1 alpha-3 codes. Empty means any country." }, "max_attempts": { "type": "integer", "minimum": 1, "maximum": 5, "description": "How many times the ID's photos are read while the person is on the page, 1 to 5." } }, "additionalProperties": false }, "rules": { "$ref": "#/components/schemas/VerifyRuleOverrides" }, "retention_days": { "type": "integer", "minimum": 1, "maximum": 365, "description": "Days after the session ends when its photos are deleted, 1 to 365." } }, "additionalProperties": false, "description": "Anything this session should do differently from its project's Verify settings, or your team's. Leave a key out to keep the setting." }, "VerifyRuleOverrides": { "type": "object", "properties": { "liveness_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_unreadable": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_expired": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `reject` unless you change it." }, "document_checksum_failed": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_type_mismatch": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_country_not_accepted": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "date_of_birth_missing": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "face_match_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_estimate_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "name_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "date_of_birth_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." } }, "additionalProperties": false }, "VerifyExpectedData": { "type": "object", "properties": { "first_name": { "type": "string", "minLength": 1, "maxLength": 100 }, "last_name": { "type": "string", "minLength": 1, "maxLength": 100 }, "date_of_birth": { "type": "string", "format": "date", "example": "1992-04-15" } }, "additionalProperties": false, "description": "What you expect the ID to say. Names match whatever their accents, case or middle names. `identity` tier only. The result is in `checks.data_match`; by default a difference sends the session to review." }, "VerifySessionList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ "list" ] }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/VerifySession" } }, "has_more": { "type": "boolean" } }, "required": [ "object", "data", "has_more" ] } }, "parameters": {} }, "paths": { "/v1/verify/sessions": { "post": { "operationId": "createVerifySession", "tags": [ "Verify" ], "summary": "Create a session", "description": "Starts a verification and returns the `url` to send the person to. A live key sets the tier's price aside, which is kept only if the session reaches a decision.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateVerifySession" } } } }, "responses": { "201": { "description": "The new session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "400": { "description": "Something in the request isn't valid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need Upsky AI Pro.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "Live checks aren't connected right now, or prices couldn't be read.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "get": { "operationId": "listVerifySessions", "tags": [ "Verify" ], "summary": "List sessions", "description": "Your team's sessions in the key's mode, newest first. Pass the last `id` as `starting_after` for the next page.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "required": false, "name": "limit", "in": "query" }, { "schema": { "type": "string", "format": "uuid" }, "required": false, "name": "starting_after", "in": "query" }, { "schema": { "type": "string", "enum": [ "pending", "in_progress", "processing", "approved", "rejected", "needs_review", "expired", "canceled" ] }, "required": false, "name": "status", "in": "query" } ], "responses": { "200": { "description": "The sessions.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySessionList" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}": { "get": { "operationId": "getVerifySession", "tags": [ "Verify" ], "summary": "Get a session", "description": "Returns a session, with what was read from the ID once it's decided.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}/cancel": { "post": { "operationId": "cancelVerifySession", "tags": [ "Verify" ], "summary": "Cancel a session", "description": "Stops a session the person hasn't submitted. Its link stops working and nothing is charged.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The canceled session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The session was already submitted or ended.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}/redact": { "post": { "operationId": "redactVerifySession", "tags": [ "Verify" ], "summary": "Redact a session", "description": "Deletes everything about the person from a session that has ended, right away: the photos, what was read from the ID, the `expected` data, and your `reference`, `metadata` and `return_url`. The status, reasons and checks stay as your record. It can't be undone. Redacting a session twice changes nothing.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The redacted session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The session hasn't ended. Cancel it first, or wait for its result.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": {} } # Get a session (/en/verify/reference/getVerifySession) { "openapi": "3.2.0", "info": { "title": "Upsky Verify API", "version": "1.0.0" }, "servers": [ { "url": "https://api.upsky.dev" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "Your API key, starting with upsky_test_ or upsky_live_." } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "invalid_request", "unauthorized", "insufficient_credits", "pro_required", "account_suspended", "forbidden", "not_found", "method_not_allowed", "conflict", "idempotency_key_reused", "request_in_progress", "retry_later", "payload_too_large", "unsupported_media_type", "rate_limited", "internal_error", "service_unavailable" ], "description": "A stable code to branch on.", "example": "unauthorized" }, "message": { "type": "string", "description": "What went wrong, for a person to read.", "example": "The API key is not valid or has been revoked." }, "request_id": { "type": "string", "description": "Include this when you contact support.", "example": "req_3f9a1c0b7d2e4a6f8b1c2d3e" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] }, "VerifySession": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "object": { "type": "string", "enum": [ "verify.session" ] }, "livemode": { "type": "boolean" }, "tier": { "type": "string", "enum": [ "age", "identity", "identity_plus" ] }, "status": { "type": "string", "enum": [ "pending", "in_progress", "processing", "approved", "rejected", "needs_review", "expired", "canceled" ], "description": "`pending` until the person opens the link, `in_progress` while they take photos, `processing` once submitted, then `approved`, `rejected` or `needs_review`. A link that's never finished ends `expired` or `canceled`, and isn't charged." }, "url": { "type": [ "string", "null" ], "description": "The link to send the person to. Null once it can't be used anymore.", "example": "https://verify.upsky.dev/s/9Qm2..." }, "reference": { "type": [ "string", "null" ] }, "metadata": { "type": "object", "additionalProperties": { "type": "string" } }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "return_url": { "type": [ "string", "null" ] }, "minimum_age": { "type": "integer" }, "allow_selfie_upload": { "type": "boolean", "description": "Whether the person may send a selfie from their photos. When false, it has to be taken with the camera on the page." }, "show_result": { "type": "boolean", "description": "Whether the link shows the person the result once it's decided." }, "thresholds": { "type": "object", "properties": { "preset": { "type": "string", "enum": [ "relaxed", "balanced", "strict", "custom" ] }, "liveness_pass": { "type": "number" }, "liveness_review": { "type": "number" }, "face_match_pass": { "type": "number" }, "face_match_review": { "type": "number" }, "age_buffer_years": { "type": "integer" } }, "required": [ "preset", "liveness_pass", "liveness_review", "face_match_pass", "face_match_review", "age_buffer_years" ], "description": "How sure each check had to be, copied when the session was created from its project's Verify settings, or your team's if the project has none. A score at or above a pass mark passes; from its review mark up to the pass mark goes to review; below the review mark is rejected." }, "options": { "$ref": "#/components/schemas/VerifySessionOptions" }, "reasons": { "type": "array", "items": { "type": "string", "enum": [ "liveness_failed", "liveness_inconclusive", "face_not_found", "age_under_minimum", "age_inconclusive", "document_unreadable", "document_expired", "document_checksum_failed", "document_type_mismatch", "document_country_not_accepted", "date_of_birth_missing", "face_mismatch", "face_match_inconclusive", "age_estimate_mismatch", "name_mismatch", "date_of_birth_mismatch", "processing_error", "reviewed" ] }, "description": "Why the session ended as it did.", "example": [ "face_mismatch" ] }, "checks": { "$ref": "#/components/schemas/VerifyChecks" }, "document_type": { "type": [ "string", "null" ], "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card", null ] }, "verified_outputs": { "$ref": "#/components/schemas/VerifiedOutputs" }, "expires_at": { "type": "string" }, "created_at": { "type": "string" }, "started_at": { "type": [ "string", "null" ] }, "submitted_at": { "type": [ "string", "null" ] }, "decided_at": { "type": [ "string", "null" ] }, "reviewed_at": { "type": [ "string", "null" ] }, "redacted_at": { "type": [ "string", "null" ], "description": "When the person's data was deleted from the session with the redact endpoint." } }, "required": [ "id", "object", "livemode", "tier", "status", "url", "reference", "metadata", "project_id", "return_url", "minimum_age", "allow_selfie_upload", "show_result", "thresholds", "options", "reasons", "checks", "document_type", "verified_outputs", "expires_at", "created_at", "started_at", "submitted_at", "decided_at", "reviewed_at", "redacted_at" ] }, "VerifySessionOptions": { "type": "object", "properties": { "documents": { "type": "object", "properties": { "allowed_types": { "type": "array", "items": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "description": "The IDs the person may use." }, "allowed_countries": { "type": "array", "items": { "type": "string" }, "description": "Issuing countries accepted, as ISO 3166-1 alpha-3 codes. Empty means any country.", "example": [ "MEX", "USA" ] }, "max_attempts": { "type": "integer", "description": "How many times the ID's photos are read while the person is on the page. A read that finds a blurry photo, the wrong kind of ID, an expired one or a country you don't accept asks them to try again, until these run out." } }, "required": [ "allowed_types", "allowed_countries", "max_attempts" ] }, "rules": { "$ref": "#/components/schemas/VerifyRules" }, "retention_days": { "type": "integer", "description": "Days after the session ends when its photos are deleted. A session waiting for review keeps them at least 7 days." } }, "required": [ "documents", "rules", "retention_days" ], "description": "What the session runs by, copied when it was created from its project's Verify settings, or your team's, with anything you set in the request on top." }, "VerifyRules": { "type": "object", "properties": { "liveness_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_unreadable": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_expired": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `reject` unless you change it." }, "document_checksum_failed": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_type_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_country_not_accepted": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "date_of_birth_missing": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "face_match_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_estimate_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "name_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "date_of_birth_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." } }, "required": [ "liveness_inconclusive", "age_inconclusive", "document_unreadable", "document_expired", "document_checksum_failed", "document_type_mismatch", "document_country_not_accepted", "date_of_birth_missing", "face_match_inconclusive", "age_estimate_mismatch", "name_mismatch", "date_of_birth_mismatch" ], "description": "What each problem leads to: `reject` the session, send it to `review`, or `ignore` it. Problems that aren't here always reject: a failed liveness check, no face, a face that isn't the ID's, or an age under the minimum." }, "VerifyChecks": { "type": "object", "properties": { "liveness": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "confidence": { "type": [ "number", "null" ], "description": "AWS Face Liveness confidence, 0 to 100." }, "feedback": { "type": "array", "items": { "type": "string", "enum": [ "face_not_visible", "face_obstructed", "face_not_aligned", "eyes_closed", "low_lighting", "high_lighting", "low_video_quality" ] }, "description": "What lowered the liveness score, such as `high_lighting`. Empty when nothing did." } }, "required": [ "status", "confidence", "feedback" ] }, "age": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "minimum_age": { "type": "integer" }, "estimated_low": { "type": [ "number", "null" ] }, "estimated_high": { "type": [ "number", "null" ] }, "estimated_age": { "type": [ "number", "null" ], "description": "The age the person looks: the middle of the estimated range. On the `age` tier it passes from `minimum_age` plus your age margin." }, "age_from_document": { "type": [ "integer", "null" ] } }, "required": [ "status", "minimum_age", "estimated_low", "estimated_high", "estimated_age", "age_from_document" ] }, "document": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "document_type": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "expired": { "type": [ "boolean", "null" ] }, "checksums_valid": { "type": [ "boolean", "null" ] }, "type_matches": { "type": [ "boolean", "null" ], "description": "Whether the ID is the kind the person chose. Null when the read couldn't tell." }, "country_accepted": { "type": [ "boolean", "null" ], "description": "Whether the ID's country is one the session accepts. Null when any country is, or the country couldn't be read." } }, "required": [ "status", "document_type", "expired", "checksums_valid", "type_matches", "country_accepted" ] }, "face_match": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "similarity": { "type": [ "number", "null" ], "description": "How alike the selfie and the ID's photo are, 0 to 100." } }, "required": [ "status", "similarity" ] }, "data_match": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "first_name": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] }, "last_name": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] }, "date_of_birth": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] } }, "required": [ "status", "first_name", "last_name", "date_of_birth" ], "description": "How the ID compared with the `expected` data you sent. Only on sessions created with it. A field is null when you expected nothing for it, and `not_read` when the ID gave nothing to compare." } }, "description": "What each check found. Empty until the session is decided." }, "VerifiedOutputs": { "type": [ "object", "null" ], "properties": { "first_name": { "type": [ "string", "null" ] }, "last_name": { "type": [ "string", "null" ] }, "date_of_birth": { "type": [ "string", "null" ], "example": "1992-04-15" }, "document_type": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "document_number": { "type": [ "string", "null" ] }, "issuing_country": { "type": [ "string", "null" ], "example": "MEX" }, "expires_on": { "type": [ "string", "null" ], "example": "2031-12-31" } }, "required": [ "first_name", "last_name", "date_of_birth", "document_type", "document_number", "issuing_country", "expires_on" ], "description": "What was read from the ID. Identity tier and above, once decided. Only returned when you read one session, never in lists or webhooks." }, "CreateVerifySession": { "type": "object", "properties": { "tier": { "type": "string", "enum": [ "age", "identity", "identity_plus" ], "default": "identity", "description": "`age`: selfie with liveness and an age estimate. `identity`: adds an ID, front and back, and a face match. `identity_plus` isn't available yet." }, "reference": { "type": "string", "minLength": 1, "maxLength": 200, "description": "Your own ID for this person. Only your team sees it.", "example": "user_123" }, "return_url": { "type": "string", "maxLength": 2000, "description": "Where to send the person once they're done: a web page, or your app through its own scheme (`myapp://verified`) to bring them back from an in-app browser. The page goes there by itself a few seconds after they submit, or after the result when `show_result` is on, and also shows a button.", "example": "https://example.com/verified" }, "minimum_age": { "type": "integer", "minimum": 13, "maximum": 25, "default": 18 }, "allow_selfie_upload": { "type": "boolean", "default": false, "description": "Let the person send a selfie from their photos instead of taking it with the camera. Camera only by default. Live sessions always take the selfie with the camera, through the liveness check." }, "show_result": { "type": "boolean", "default": false, "description": "Show the person the result on the link right after they send it: approved, rejected or in review, never why. Off by default, so you tell them yourself." }, "expires_in": { "type": "integer", "minimum": 3600, "maximum": 604800, "default": 86400, "description": "Seconds the link works for, from an hour to seven days. A day by default." }, "project_id": { "type": "string", "format": "uuid", "description": "One of your team's projects, to group sessions by. The session uses the project's Verify settings where it has its own." }, "options": { "$ref": "#/components/schemas/CreateVerifySessionOptions" }, "expected": { "$ref": "#/components/schemas/VerifyExpectedData" }, "metadata": { "type": "object", "additionalProperties": { "type": "string", "maxLength": 500 }, "description": "Up to 20 keys of your own, returned as given." } } }, "CreateVerifySessionOptions": { "type": "object", "properties": { "documents": { "type": "object", "properties": { "allowed_types": { "type": "array", "items": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "minItems": 1, "description": "The IDs the person may use." }, "allowed_countries": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z]{3}$", "example": "MEX" }, "maxItems": 250, "description": "Issuing countries accepted, as ISO 3166-1 alpha-3 codes. Empty means any country." }, "max_attempts": { "type": "integer", "minimum": 1, "maximum": 5, "description": "How many times the ID's photos are read while the person is on the page, 1 to 5." } }, "additionalProperties": false }, "rules": { "$ref": "#/components/schemas/VerifyRuleOverrides" }, "retention_days": { "type": "integer", "minimum": 1, "maximum": 365, "description": "Days after the session ends when its photos are deleted, 1 to 365." } }, "additionalProperties": false, "description": "Anything this session should do differently from its project's Verify settings, or your team's. Leave a key out to keep the setting." }, "VerifyRuleOverrides": { "type": "object", "properties": { "liveness_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_unreadable": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_expired": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `reject` unless you change it." }, "document_checksum_failed": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_type_mismatch": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_country_not_accepted": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "date_of_birth_missing": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "face_match_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_estimate_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "name_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "date_of_birth_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." } }, "additionalProperties": false }, "VerifyExpectedData": { "type": "object", "properties": { "first_name": { "type": "string", "minLength": 1, "maxLength": 100 }, "last_name": { "type": "string", "minLength": 1, "maxLength": 100 }, "date_of_birth": { "type": "string", "format": "date", "example": "1992-04-15" } }, "additionalProperties": false, "description": "What you expect the ID to say. Names match whatever their accents, case or middle names. `identity` tier only. The result is in `checks.data_match`; by default a difference sends the session to review." }, "VerifySessionList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ "list" ] }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/VerifySession" } }, "has_more": { "type": "boolean" } }, "required": [ "object", "data", "has_more" ] } }, "parameters": {} }, "paths": { "/v1/verify/sessions": { "post": { "operationId": "createVerifySession", "tags": [ "Verify" ], "summary": "Create a session", "description": "Starts a verification and returns the `url` to send the person to. A live key sets the tier's price aside, which is kept only if the session reaches a decision.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateVerifySession" } } } }, "responses": { "201": { "description": "The new session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "400": { "description": "Something in the request isn't valid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need Upsky AI Pro.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "Live checks aren't connected right now, or prices couldn't be read.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "get": { "operationId": "listVerifySessions", "tags": [ "Verify" ], "summary": "List sessions", "description": "Your team's sessions in the key's mode, newest first. Pass the last `id` as `starting_after` for the next page.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "required": false, "name": "limit", "in": "query" }, { "schema": { "type": "string", "format": "uuid" }, "required": false, "name": "starting_after", "in": "query" }, { "schema": { "type": "string", "enum": [ "pending", "in_progress", "processing", "approved", "rejected", "needs_review", "expired", "canceled" ] }, "required": false, "name": "status", "in": "query" } ], "responses": { "200": { "description": "The sessions.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySessionList" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}": { "get": { "operationId": "getVerifySession", "tags": [ "Verify" ], "summary": "Get a session", "description": "Returns a session, with what was read from the ID once it's decided.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}/cancel": { "post": { "operationId": "cancelVerifySession", "tags": [ "Verify" ], "summary": "Cancel a session", "description": "Stops a session the person hasn't submitted. Its link stops working and nothing is charged.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The canceled session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The session was already submitted or ended.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}/redact": { "post": { "operationId": "redactVerifySession", "tags": [ "Verify" ], "summary": "Redact a session", "description": "Deletes everything about the person from a session that has ended, right away: the photos, what was read from the ID, the `expected` data, and your `reference`, `metadata` and `return_url`. The status, reasons and checks stay as your record. It can't be undone. Redacting a session twice changes nothing.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The redacted session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The session hasn't ended. Cancel it first, or wait for its result.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": {} } # Cancel a session (/en/verify/reference/cancelVerifySession) { "openapi": "3.2.0", "info": { "title": "Upsky Verify API", "version": "1.0.0" }, "servers": [ { "url": "https://api.upsky.dev" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "Your API key, starting with upsky_test_ or upsky_live_." } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "invalid_request", "unauthorized", "insufficient_credits", "pro_required", "account_suspended", "forbidden", "not_found", "method_not_allowed", "conflict", "idempotency_key_reused", "request_in_progress", "retry_later", "payload_too_large", "unsupported_media_type", "rate_limited", "internal_error", "service_unavailable" ], "description": "A stable code to branch on.", "example": "unauthorized" }, "message": { "type": "string", "description": "What went wrong, for a person to read.", "example": "The API key is not valid or has been revoked." }, "request_id": { "type": "string", "description": "Include this when you contact support.", "example": "req_3f9a1c0b7d2e4a6f8b1c2d3e" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] }, "VerifySession": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "object": { "type": "string", "enum": [ "verify.session" ] }, "livemode": { "type": "boolean" }, "tier": { "type": "string", "enum": [ "age", "identity", "identity_plus" ] }, "status": { "type": "string", "enum": [ "pending", "in_progress", "processing", "approved", "rejected", "needs_review", "expired", "canceled" ], "description": "`pending` until the person opens the link, `in_progress` while they take photos, `processing` once submitted, then `approved`, `rejected` or `needs_review`. A link that's never finished ends `expired` or `canceled`, and isn't charged." }, "url": { "type": [ "string", "null" ], "description": "The link to send the person to. Null once it can't be used anymore.", "example": "https://verify.upsky.dev/s/9Qm2..." }, "reference": { "type": [ "string", "null" ] }, "metadata": { "type": "object", "additionalProperties": { "type": "string" } }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "return_url": { "type": [ "string", "null" ] }, "minimum_age": { "type": "integer" }, "allow_selfie_upload": { "type": "boolean", "description": "Whether the person may send a selfie from their photos. When false, it has to be taken with the camera on the page." }, "show_result": { "type": "boolean", "description": "Whether the link shows the person the result once it's decided." }, "thresholds": { "type": "object", "properties": { "preset": { "type": "string", "enum": [ "relaxed", "balanced", "strict", "custom" ] }, "liveness_pass": { "type": "number" }, "liveness_review": { "type": "number" }, "face_match_pass": { "type": "number" }, "face_match_review": { "type": "number" }, "age_buffer_years": { "type": "integer" } }, "required": [ "preset", "liveness_pass", "liveness_review", "face_match_pass", "face_match_review", "age_buffer_years" ], "description": "How sure each check had to be, copied when the session was created from its project's Verify settings, or your team's if the project has none. A score at or above a pass mark passes; from its review mark up to the pass mark goes to review; below the review mark is rejected." }, "options": { "$ref": "#/components/schemas/VerifySessionOptions" }, "reasons": { "type": "array", "items": { "type": "string", "enum": [ "liveness_failed", "liveness_inconclusive", "face_not_found", "age_under_minimum", "age_inconclusive", "document_unreadable", "document_expired", "document_checksum_failed", "document_type_mismatch", "document_country_not_accepted", "date_of_birth_missing", "face_mismatch", "face_match_inconclusive", "age_estimate_mismatch", "name_mismatch", "date_of_birth_mismatch", "processing_error", "reviewed" ] }, "description": "Why the session ended as it did.", "example": [ "face_mismatch" ] }, "checks": { "$ref": "#/components/schemas/VerifyChecks" }, "document_type": { "type": [ "string", "null" ], "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card", null ] }, "verified_outputs": { "$ref": "#/components/schemas/VerifiedOutputs" }, "expires_at": { "type": "string" }, "created_at": { "type": "string" }, "started_at": { "type": [ "string", "null" ] }, "submitted_at": { "type": [ "string", "null" ] }, "decided_at": { "type": [ "string", "null" ] }, "reviewed_at": { "type": [ "string", "null" ] }, "redacted_at": { "type": [ "string", "null" ], "description": "When the person's data was deleted from the session with the redact endpoint." } }, "required": [ "id", "object", "livemode", "tier", "status", "url", "reference", "metadata", "project_id", "return_url", "minimum_age", "allow_selfie_upload", "show_result", "thresholds", "options", "reasons", "checks", "document_type", "verified_outputs", "expires_at", "created_at", "started_at", "submitted_at", "decided_at", "reviewed_at", "redacted_at" ] }, "VerifySessionOptions": { "type": "object", "properties": { "documents": { "type": "object", "properties": { "allowed_types": { "type": "array", "items": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "description": "The IDs the person may use." }, "allowed_countries": { "type": "array", "items": { "type": "string" }, "description": "Issuing countries accepted, as ISO 3166-1 alpha-3 codes. Empty means any country.", "example": [ "MEX", "USA" ] }, "max_attempts": { "type": "integer", "description": "How many times the ID's photos are read while the person is on the page. A read that finds a blurry photo, the wrong kind of ID, an expired one or a country you don't accept asks them to try again, until these run out." } }, "required": [ "allowed_types", "allowed_countries", "max_attempts" ] }, "rules": { "$ref": "#/components/schemas/VerifyRules" }, "retention_days": { "type": "integer", "description": "Days after the session ends when its photos are deleted. A session waiting for review keeps them at least 7 days." } }, "required": [ "documents", "rules", "retention_days" ], "description": "What the session runs by, copied when it was created from its project's Verify settings, or your team's, with anything you set in the request on top." }, "VerifyRules": { "type": "object", "properties": { "liveness_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_unreadable": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_expired": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `reject` unless you change it." }, "document_checksum_failed": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_type_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_country_not_accepted": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "date_of_birth_missing": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "face_match_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_estimate_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "name_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "date_of_birth_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." } }, "required": [ "liveness_inconclusive", "age_inconclusive", "document_unreadable", "document_expired", "document_checksum_failed", "document_type_mismatch", "document_country_not_accepted", "date_of_birth_missing", "face_match_inconclusive", "age_estimate_mismatch", "name_mismatch", "date_of_birth_mismatch" ], "description": "What each problem leads to: `reject` the session, send it to `review`, or `ignore` it. Problems that aren't here always reject: a failed liveness check, no face, a face that isn't the ID's, or an age under the minimum." }, "VerifyChecks": { "type": "object", "properties": { "liveness": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "confidence": { "type": [ "number", "null" ], "description": "AWS Face Liveness confidence, 0 to 100." }, "feedback": { "type": "array", "items": { "type": "string", "enum": [ "face_not_visible", "face_obstructed", "face_not_aligned", "eyes_closed", "low_lighting", "high_lighting", "low_video_quality" ] }, "description": "What lowered the liveness score, such as `high_lighting`. Empty when nothing did." } }, "required": [ "status", "confidence", "feedback" ] }, "age": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "minimum_age": { "type": "integer" }, "estimated_low": { "type": [ "number", "null" ] }, "estimated_high": { "type": [ "number", "null" ] }, "estimated_age": { "type": [ "number", "null" ], "description": "The age the person looks: the middle of the estimated range. On the `age` tier it passes from `minimum_age` plus your age margin." }, "age_from_document": { "type": [ "integer", "null" ] } }, "required": [ "status", "minimum_age", "estimated_low", "estimated_high", "estimated_age", "age_from_document" ] }, "document": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "document_type": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "expired": { "type": [ "boolean", "null" ] }, "checksums_valid": { "type": [ "boolean", "null" ] }, "type_matches": { "type": [ "boolean", "null" ], "description": "Whether the ID is the kind the person chose. Null when the read couldn't tell." }, "country_accepted": { "type": [ "boolean", "null" ], "description": "Whether the ID's country is one the session accepts. Null when any country is, or the country couldn't be read." } }, "required": [ "status", "document_type", "expired", "checksums_valid", "type_matches", "country_accepted" ] }, "face_match": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "similarity": { "type": [ "number", "null" ], "description": "How alike the selfie and the ID's photo are, 0 to 100." } }, "required": [ "status", "similarity" ] }, "data_match": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "first_name": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] }, "last_name": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] }, "date_of_birth": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] } }, "required": [ "status", "first_name", "last_name", "date_of_birth" ], "description": "How the ID compared with the `expected` data you sent. Only on sessions created with it. A field is null when you expected nothing for it, and `not_read` when the ID gave nothing to compare." } }, "description": "What each check found. Empty until the session is decided." }, "VerifiedOutputs": { "type": [ "object", "null" ], "properties": { "first_name": { "type": [ "string", "null" ] }, "last_name": { "type": [ "string", "null" ] }, "date_of_birth": { "type": [ "string", "null" ], "example": "1992-04-15" }, "document_type": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "document_number": { "type": [ "string", "null" ] }, "issuing_country": { "type": [ "string", "null" ], "example": "MEX" }, "expires_on": { "type": [ "string", "null" ], "example": "2031-12-31" } }, "required": [ "first_name", "last_name", "date_of_birth", "document_type", "document_number", "issuing_country", "expires_on" ], "description": "What was read from the ID. Identity tier and above, once decided. Only returned when you read one session, never in lists or webhooks." }, "CreateVerifySession": { "type": "object", "properties": { "tier": { "type": "string", "enum": [ "age", "identity", "identity_plus" ], "default": "identity", "description": "`age`: selfie with liveness and an age estimate. `identity`: adds an ID, front and back, and a face match. `identity_plus` isn't available yet." }, "reference": { "type": "string", "minLength": 1, "maxLength": 200, "description": "Your own ID for this person. Only your team sees it.", "example": "user_123" }, "return_url": { "type": "string", "maxLength": 2000, "description": "Where to send the person once they're done: a web page, or your app through its own scheme (`myapp://verified`) to bring them back from an in-app browser. The page goes there by itself a few seconds after they submit, or after the result when `show_result` is on, and also shows a button.", "example": "https://example.com/verified" }, "minimum_age": { "type": "integer", "minimum": 13, "maximum": 25, "default": 18 }, "allow_selfie_upload": { "type": "boolean", "default": false, "description": "Let the person send a selfie from their photos instead of taking it with the camera. Camera only by default. Live sessions always take the selfie with the camera, through the liveness check." }, "show_result": { "type": "boolean", "default": false, "description": "Show the person the result on the link right after they send it: approved, rejected or in review, never why. Off by default, so you tell them yourself." }, "expires_in": { "type": "integer", "minimum": 3600, "maximum": 604800, "default": 86400, "description": "Seconds the link works for, from an hour to seven days. A day by default." }, "project_id": { "type": "string", "format": "uuid", "description": "One of your team's projects, to group sessions by. The session uses the project's Verify settings where it has its own." }, "options": { "$ref": "#/components/schemas/CreateVerifySessionOptions" }, "expected": { "$ref": "#/components/schemas/VerifyExpectedData" }, "metadata": { "type": "object", "additionalProperties": { "type": "string", "maxLength": 500 }, "description": "Up to 20 keys of your own, returned as given." } } }, "CreateVerifySessionOptions": { "type": "object", "properties": { "documents": { "type": "object", "properties": { "allowed_types": { "type": "array", "items": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "minItems": 1, "description": "The IDs the person may use." }, "allowed_countries": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z]{3}$", "example": "MEX" }, "maxItems": 250, "description": "Issuing countries accepted, as ISO 3166-1 alpha-3 codes. Empty means any country." }, "max_attempts": { "type": "integer", "minimum": 1, "maximum": 5, "description": "How many times the ID's photos are read while the person is on the page, 1 to 5." } }, "additionalProperties": false }, "rules": { "$ref": "#/components/schemas/VerifyRuleOverrides" }, "retention_days": { "type": "integer", "minimum": 1, "maximum": 365, "description": "Days after the session ends when its photos are deleted, 1 to 365." } }, "additionalProperties": false, "description": "Anything this session should do differently from its project's Verify settings, or your team's. Leave a key out to keep the setting." }, "VerifyRuleOverrides": { "type": "object", "properties": { "liveness_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_unreadable": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_expired": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `reject` unless you change it." }, "document_checksum_failed": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_type_mismatch": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_country_not_accepted": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "date_of_birth_missing": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "face_match_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_estimate_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "name_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "date_of_birth_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." } }, "additionalProperties": false }, "VerifyExpectedData": { "type": "object", "properties": { "first_name": { "type": "string", "minLength": 1, "maxLength": 100 }, "last_name": { "type": "string", "minLength": 1, "maxLength": 100 }, "date_of_birth": { "type": "string", "format": "date", "example": "1992-04-15" } }, "additionalProperties": false, "description": "What you expect the ID to say. Names match whatever their accents, case or middle names. `identity` tier only. The result is in `checks.data_match`; by default a difference sends the session to review." }, "VerifySessionList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ "list" ] }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/VerifySession" } }, "has_more": { "type": "boolean" } }, "required": [ "object", "data", "has_more" ] } }, "parameters": {} }, "paths": { "/v1/verify/sessions": { "post": { "operationId": "createVerifySession", "tags": [ "Verify" ], "summary": "Create a session", "description": "Starts a verification and returns the `url` to send the person to. A live key sets the tier's price aside, which is kept only if the session reaches a decision.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateVerifySession" } } } }, "responses": { "201": { "description": "The new session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "400": { "description": "Something in the request isn't valid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need Upsky AI Pro.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "Live checks aren't connected right now, or prices couldn't be read.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "get": { "operationId": "listVerifySessions", "tags": [ "Verify" ], "summary": "List sessions", "description": "Your team's sessions in the key's mode, newest first. Pass the last `id` as `starting_after` for the next page.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "required": false, "name": "limit", "in": "query" }, { "schema": { "type": "string", "format": "uuid" }, "required": false, "name": "starting_after", "in": "query" }, { "schema": { "type": "string", "enum": [ "pending", "in_progress", "processing", "approved", "rejected", "needs_review", "expired", "canceled" ] }, "required": false, "name": "status", "in": "query" } ], "responses": { "200": { "description": "The sessions.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySessionList" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}": { "get": { "operationId": "getVerifySession", "tags": [ "Verify" ], "summary": "Get a session", "description": "Returns a session, with what was read from the ID once it's decided.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}/cancel": { "post": { "operationId": "cancelVerifySession", "tags": [ "Verify" ], "summary": "Cancel a session", "description": "Stops a session the person hasn't submitted. Its link stops working and nothing is charged.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The canceled session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The session was already submitted or ended.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}/redact": { "post": { "operationId": "redactVerifySession", "tags": [ "Verify" ], "summary": "Redact a session", "description": "Deletes everything about the person from a session that has ended, right away: the photos, what was read from the ID, the `expected` data, and your `reference`, `metadata` and `return_url`. The status, reasons and checks stay as your record. It can't be undone. Redacting a session twice changes nothing.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The redacted session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The session hasn't ended. Cancel it first, or wait for its result.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": {} } # Redact a session (/en/verify/reference/redactVerifySession) { "openapi": "3.2.0", "info": { "title": "Upsky Verify API", "version": "1.0.0" }, "servers": [ { "url": "https://api.upsky.dev" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "Your API key, starting with upsky_test_ or upsky_live_." } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "invalid_request", "unauthorized", "insufficient_credits", "pro_required", "account_suspended", "forbidden", "not_found", "method_not_allowed", "conflict", "idempotency_key_reused", "request_in_progress", "retry_later", "payload_too_large", "unsupported_media_type", "rate_limited", "internal_error", "service_unavailable" ], "description": "A stable code to branch on.", "example": "unauthorized" }, "message": { "type": "string", "description": "What went wrong, for a person to read.", "example": "The API key is not valid or has been revoked." }, "request_id": { "type": "string", "description": "Include this when you contact support.", "example": "req_3f9a1c0b7d2e4a6f8b1c2d3e" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] }, "VerifySession": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "object": { "type": "string", "enum": [ "verify.session" ] }, "livemode": { "type": "boolean" }, "tier": { "type": "string", "enum": [ "age", "identity", "identity_plus" ] }, "status": { "type": "string", "enum": [ "pending", "in_progress", "processing", "approved", "rejected", "needs_review", "expired", "canceled" ], "description": "`pending` until the person opens the link, `in_progress` while they take photos, `processing` once submitted, then `approved`, `rejected` or `needs_review`. A link that's never finished ends `expired` or `canceled`, and isn't charged." }, "url": { "type": [ "string", "null" ], "description": "The link to send the person to. Null once it can't be used anymore.", "example": "https://verify.upsky.dev/s/9Qm2..." }, "reference": { "type": [ "string", "null" ] }, "metadata": { "type": "object", "additionalProperties": { "type": "string" } }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "return_url": { "type": [ "string", "null" ] }, "minimum_age": { "type": "integer" }, "allow_selfie_upload": { "type": "boolean", "description": "Whether the person may send a selfie from their photos. When false, it has to be taken with the camera on the page." }, "show_result": { "type": "boolean", "description": "Whether the link shows the person the result once it's decided." }, "thresholds": { "type": "object", "properties": { "preset": { "type": "string", "enum": [ "relaxed", "balanced", "strict", "custom" ] }, "liveness_pass": { "type": "number" }, "liveness_review": { "type": "number" }, "face_match_pass": { "type": "number" }, "face_match_review": { "type": "number" }, "age_buffer_years": { "type": "integer" } }, "required": [ "preset", "liveness_pass", "liveness_review", "face_match_pass", "face_match_review", "age_buffer_years" ], "description": "How sure each check had to be, copied when the session was created from its project's Verify settings, or your team's if the project has none. A score at or above a pass mark passes; from its review mark up to the pass mark goes to review; below the review mark is rejected." }, "options": { "$ref": "#/components/schemas/VerifySessionOptions" }, "reasons": { "type": "array", "items": { "type": "string", "enum": [ "liveness_failed", "liveness_inconclusive", "face_not_found", "age_under_minimum", "age_inconclusive", "document_unreadable", "document_expired", "document_checksum_failed", "document_type_mismatch", "document_country_not_accepted", "date_of_birth_missing", "face_mismatch", "face_match_inconclusive", "age_estimate_mismatch", "name_mismatch", "date_of_birth_mismatch", "processing_error", "reviewed" ] }, "description": "Why the session ended as it did.", "example": [ "face_mismatch" ] }, "checks": { "$ref": "#/components/schemas/VerifyChecks" }, "document_type": { "type": [ "string", "null" ], "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card", null ] }, "verified_outputs": { "$ref": "#/components/schemas/VerifiedOutputs" }, "expires_at": { "type": "string" }, "created_at": { "type": "string" }, "started_at": { "type": [ "string", "null" ] }, "submitted_at": { "type": [ "string", "null" ] }, "decided_at": { "type": [ "string", "null" ] }, "reviewed_at": { "type": [ "string", "null" ] }, "redacted_at": { "type": [ "string", "null" ], "description": "When the person's data was deleted from the session with the redact endpoint." } }, "required": [ "id", "object", "livemode", "tier", "status", "url", "reference", "metadata", "project_id", "return_url", "minimum_age", "allow_selfie_upload", "show_result", "thresholds", "options", "reasons", "checks", "document_type", "verified_outputs", "expires_at", "created_at", "started_at", "submitted_at", "decided_at", "reviewed_at", "redacted_at" ] }, "VerifySessionOptions": { "type": "object", "properties": { "documents": { "type": "object", "properties": { "allowed_types": { "type": "array", "items": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "description": "The IDs the person may use." }, "allowed_countries": { "type": "array", "items": { "type": "string" }, "description": "Issuing countries accepted, as ISO 3166-1 alpha-3 codes. Empty means any country.", "example": [ "MEX", "USA" ] }, "max_attempts": { "type": "integer", "description": "How many times the ID's photos are read while the person is on the page. A read that finds a blurry photo, the wrong kind of ID, an expired one or a country you don't accept asks them to try again, until these run out." } }, "required": [ "allowed_types", "allowed_countries", "max_attempts" ] }, "rules": { "$ref": "#/components/schemas/VerifyRules" }, "retention_days": { "type": "integer", "description": "Days after the session ends when its photos are deleted. A session waiting for review keeps them at least 7 days." } }, "required": [ "documents", "rules", "retention_days" ], "description": "What the session runs by, copied when it was created from its project's Verify settings, or your team's, with anything you set in the request on top." }, "VerifyRules": { "type": "object", "properties": { "liveness_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_unreadable": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_expired": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `reject` unless you change it." }, "document_checksum_failed": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_type_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_country_not_accepted": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "date_of_birth_missing": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "face_match_inconclusive": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_estimate_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "name_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "date_of_birth_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." } }, "required": [ "liveness_inconclusive", "age_inconclusive", "document_unreadable", "document_expired", "document_checksum_failed", "document_type_mismatch", "document_country_not_accepted", "date_of_birth_missing", "face_match_inconclusive", "age_estimate_mismatch", "name_mismatch", "date_of_birth_mismatch" ], "description": "What each problem leads to: `reject` the session, send it to `review`, or `ignore` it. Problems that aren't here always reject: a failed liveness check, no face, a face that isn't the ID's, or an age under the minimum." }, "VerifyChecks": { "type": "object", "properties": { "liveness": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "confidence": { "type": [ "number", "null" ], "description": "AWS Face Liveness confidence, 0 to 100." }, "feedback": { "type": "array", "items": { "type": "string", "enum": [ "face_not_visible", "face_obstructed", "face_not_aligned", "eyes_closed", "low_lighting", "high_lighting", "low_video_quality" ] }, "description": "What lowered the liveness score, such as `high_lighting`. Empty when nothing did." } }, "required": [ "status", "confidence", "feedback" ] }, "age": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "minimum_age": { "type": "integer" }, "estimated_low": { "type": [ "number", "null" ] }, "estimated_high": { "type": [ "number", "null" ] }, "estimated_age": { "type": [ "number", "null" ], "description": "The age the person looks: the middle of the estimated range. On the `age` tier it passes from `minimum_age` plus your age margin." }, "age_from_document": { "type": [ "integer", "null" ] } }, "required": [ "status", "minimum_age", "estimated_low", "estimated_high", "estimated_age", "age_from_document" ] }, "document": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "document_type": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "expired": { "type": [ "boolean", "null" ] }, "checksums_valid": { "type": [ "boolean", "null" ] }, "type_matches": { "type": [ "boolean", "null" ], "description": "Whether the ID is the kind the person chose. Null when the read couldn't tell." }, "country_accepted": { "type": [ "boolean", "null" ], "description": "Whether the ID's country is one the session accepts. Null when any country is, or the country couldn't be read." } }, "required": [ "status", "document_type", "expired", "checksums_valid", "type_matches", "country_accepted" ] }, "face_match": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "similarity": { "type": [ "number", "null" ], "description": "How alike the selfie and the ID's photo are, 0 to 100." } }, "required": [ "status", "similarity" ] }, "data_match": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "passed", "failed", "inconclusive" ] }, "first_name": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] }, "last_name": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] }, "date_of_birth": { "type": [ "string", "null" ], "enum": [ "match", "mismatch", "not_read", null ] } }, "required": [ "status", "first_name", "last_name", "date_of_birth" ], "description": "How the ID compared with the `expected` data you sent. Only on sessions created with it. A field is null when you expected nothing for it, and `not_read` when the ID gave nothing to compare." } }, "description": "What each check found. Empty until the session is decided." }, "VerifiedOutputs": { "type": [ "object", "null" ], "properties": { "first_name": { "type": [ "string", "null" ] }, "last_name": { "type": [ "string", "null" ] }, "date_of_birth": { "type": [ "string", "null" ], "example": "1992-04-15" }, "document_type": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "document_number": { "type": [ "string", "null" ] }, "issuing_country": { "type": [ "string", "null" ], "example": "MEX" }, "expires_on": { "type": [ "string", "null" ], "example": "2031-12-31" } }, "required": [ "first_name", "last_name", "date_of_birth", "document_type", "document_number", "issuing_country", "expires_on" ], "description": "What was read from the ID. Identity tier and above, once decided. Only returned when you read one session, never in lists or webhooks." }, "CreateVerifySession": { "type": "object", "properties": { "tier": { "type": "string", "enum": [ "age", "identity", "identity_plus" ], "default": "identity", "description": "`age`: selfie with liveness and an age estimate. `identity`: adds an ID, front and back, and a face match. `identity_plus` isn't available yet." }, "reference": { "type": "string", "minLength": 1, "maxLength": 200, "description": "Your own ID for this person. Only your team sees it.", "example": "user_123" }, "return_url": { "type": "string", "maxLength": 2000, "description": "Where to send the person once they're done: a web page, or your app through its own scheme (`myapp://verified`) to bring them back from an in-app browser. The page goes there by itself a few seconds after they submit, or after the result when `show_result` is on, and also shows a button.", "example": "https://example.com/verified" }, "minimum_age": { "type": "integer", "minimum": 13, "maximum": 25, "default": 18 }, "allow_selfie_upload": { "type": "boolean", "default": false, "description": "Let the person send a selfie from their photos instead of taking it with the camera. Camera only by default. Live sessions always take the selfie with the camera, through the liveness check." }, "show_result": { "type": "boolean", "default": false, "description": "Show the person the result on the link right after they send it: approved, rejected or in review, never why. Off by default, so you tell them yourself." }, "expires_in": { "type": "integer", "minimum": 3600, "maximum": 604800, "default": 86400, "description": "Seconds the link works for, from an hour to seven days. A day by default." }, "project_id": { "type": "string", "format": "uuid", "description": "One of your team's projects, to group sessions by. The session uses the project's Verify settings where it has its own." }, "options": { "$ref": "#/components/schemas/CreateVerifySessionOptions" }, "expected": { "$ref": "#/components/schemas/VerifyExpectedData" }, "metadata": { "type": "object", "additionalProperties": { "type": "string", "maxLength": 500 }, "description": "Up to 20 keys of your own, returned as given." } } }, "CreateVerifySessionOptions": { "type": "object", "properties": { "documents": { "type": "object", "properties": { "allowed_types": { "type": "array", "items": { "type": "string", "enum": [ "mx_ine", "passport", "us_drivers_license", "id_card" ] }, "minItems": 1, "description": "The IDs the person may use." }, "allowed_countries": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z]{3}$", "example": "MEX" }, "maxItems": 250, "description": "Issuing countries accepted, as ISO 3166-1 alpha-3 codes. Empty means any country." }, "max_attempts": { "type": "integer", "minimum": 1, "maximum": 5, "description": "How many times the ID's photos are read while the person is on the page, 1 to 5." } }, "additionalProperties": false }, "rules": { "$ref": "#/components/schemas/VerifyRuleOverrides" }, "retention_days": { "type": "integer", "minimum": 1, "maximum": 365, "description": "Days after the session ends when its photos are deleted, 1 to 365." } }, "additionalProperties": false, "description": "Anything this session should do differently from its project's Verify settings, or your team's. Leave a key out to keep the setting." }, "VerifyRuleOverrides": { "type": "object", "properties": { "liveness_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_unreadable": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_expired": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `reject` unless you change it." }, "document_checksum_failed": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "document_type_mismatch": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "document_country_not_accepted": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `reject` unless you change it." }, "date_of_birth_missing": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "face_match_inconclusive": { "type": "string", "enum": [ "reject", "review" ], "description": "One of `reject`, `review`. `review` unless you change it." }, "age_estimate_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "name_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." }, "date_of_birth_mismatch": { "type": "string", "enum": [ "reject", "review", "ignore" ], "description": "One of `reject`, `review`, `ignore`. `review` unless you change it." } }, "additionalProperties": false }, "VerifyExpectedData": { "type": "object", "properties": { "first_name": { "type": "string", "minLength": 1, "maxLength": 100 }, "last_name": { "type": "string", "minLength": 1, "maxLength": 100 }, "date_of_birth": { "type": "string", "format": "date", "example": "1992-04-15" } }, "additionalProperties": false, "description": "What you expect the ID to say. Names match whatever their accents, case or middle names. `identity` tier only. The result is in `checks.data_match`; by default a difference sends the session to review." }, "VerifySessionList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ "list" ] }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/VerifySession" } }, "has_more": { "type": "boolean" } }, "required": [ "object", "data", "has_more" ] } }, "parameters": {} }, "paths": { "/v1/verify/sessions": { "post": { "operationId": "createVerifySession", "tags": [ "Verify" ], "summary": "Create a session", "description": "Starts a verification and returns the `url` to send the person to. A live key sets the tier's price aside, which is kept only if the session reaches a decision.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateVerifySession" } } } }, "responses": { "201": { "description": "The new session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "400": { "description": "Something in the request isn't valid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need Upsky AI Pro.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "Live checks aren't connected right now, or prices couldn't be read.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "get": { "operationId": "listVerifySessions", "tags": [ "Verify" ], "summary": "List sessions", "description": "Your team's sessions in the key's mode, newest first. Pass the last `id` as `starting_after` for the next page.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "required": false, "name": "limit", "in": "query" }, { "schema": { "type": "string", "format": "uuid" }, "required": false, "name": "starting_after", "in": "query" }, { "schema": { "type": "string", "enum": [ "pending", "in_progress", "processing", "approved", "rejected", "needs_review", "expired", "canceled" ] }, "required": false, "name": "status", "in": "query" } ], "responses": { "200": { "description": "The sessions.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySessionList" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}": { "get": { "operationId": "getVerifySession", "tags": [ "Verify" ], "summary": "Get a session", "description": "Returns a session, with what was read from the ID once it's decided.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}/cancel": { "post": { "operationId": "cancelVerifySession", "tags": [ "Verify" ], "summary": "Cancel a session", "description": "Stops a session the person hasn't submitted. Its link stops working and nothing is charged.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The canceled session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The session was already submitted or ended.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/verify/sessions/{id}/redact": { "post": { "operationId": "redactVerifySession", "tags": [ "Verify" ], "summary": "Redact a session", "description": "Deletes everything about the person from a session that has ended, right away: the photos, what was read from the ID, the `expected` data, and your `reference`, `metadata` and `return_url`. The status, reasons and checks stay as your record. It can't be undone. Redacting a session twice changes nothing.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "format": "uuid" }, "required": true, "name": "id", "in": "path" } ], "responses": { "200": { "description": "The redacted session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifySession" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The session hasn't ended. Cancel it first, or wait for its result.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": {} } # List models (/en/ai/reference/listModels) { "openapi": "3.2.0", "info": { "title": "Upsky AI API", "version": "1.0.0" }, "servers": [ { "url": "https://api.upsky.dev" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "Your API key, starting with upsky_test_ or upsky_live_." } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "invalid_request", "unauthorized", "insufficient_credits", "pro_required", "account_suspended", "forbidden", "not_found", "method_not_allowed", "conflict", "idempotency_key_reused", "request_in_progress", "retry_later", "payload_too_large", "unsupported_media_type", "rate_limited", "internal_error", "service_unavailable" ], "description": "A stable code to branch on.", "example": "unauthorized" }, "message": { "type": "string", "description": "What went wrong, for a person to read.", "example": "The API key is not valid or has been revoked." }, "request_id": { "type": "string", "description": "Include this when you contact support.", "example": "req_3f9a1c0b7d2e4a6f8b1c2d3e" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] }, "ModelList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ "list" ] }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Model" } } }, "required": [ "object", "data" ] }, "Model": { "type": "object", "properties": { "id": { "type": "string", "description": "Send this as `model`. Always `provider/model-name`.", "example": "anthropic/claude-sonnet-5" }, "object": { "type": "string", "enum": [ "model" ] }, "type": { "type": "string", "enum": [ "text", "image", "speech" ], "description": "Which endpoint calls it: chat completions, image generations or speech.", "example": "text" }, "created": { "type": "integer", "description": "Seconds since the epoch." }, "owned_by": { "type": "string", "example": "anthropic" }, "name": { "type": "string", "example": "Claude Sonnet 5" }, "context_window": { "type": [ "integer", "null" ], "description": "The most tokens the model reads, prompt and reply together. Null for image and speech models." }, "max_output_tokens": { "type": [ "integer", "null" ], "description": "The longest reply the model writes, in tokens. Null for image and speech models." }, "pricing": { "$ref": "#/components/schemas/ModelPricing" } }, "required": [ "id", "object", "type", "created", "owned_by", "name", "context_window", "max_output_tokens", "pricing" ] }, "ModelPricing": { "type": "object", "properties": { "input": { "type": "number", "description": "Per million prompt tokens.", "example": 3.3 }, "output": { "type": "number", "description": "Per million tokens the model writes.", "example": 16.5 }, "cached_input": { "type": [ "number", "null" ], "description": "Per million prompt tokens read from the cache.", "example": 0.33 }, "image": { "type": "number", "description": "Per image, at the model's default size.", "example": 0.0528 }, "image_sizes": { "type": "array", "items": { "type": "object", "properties": { "size": { "type": "string" }, "image": { "type": "number" } }, "required": [ "size", "image" ] }, "description": "Per image at each size the model lists, smallest first. A size in between costs what the next one up does." }, "characters": { "type": "number", "description": "Per million characters of input.", "example": 16.5 } }, "description": "US dollars, with the fields that apply to the model's type. Text models, and image models without a price per image, are priced per million tokens; some charge more for very long prompts. Image models are priced per image and speech models per character." }, "ChatCompletion": { "type": "object", "properties": { "id": { "type": "string" }, "object": { "type": "string", "enum": [ "chat.completion" ] }, "created": { "type": "integer" }, "model": { "type": "string" }, "choices": { "type": "array", "items": { "type": "object", "properties": { "index": { "type": "integer" }, "message": { "type": "object", "properties": { "role": { "type": "string" }, "content": { "type": [ "string", "null" ] } }, "required": [ "role", "content" ], "additionalProperties": {} }, "finish_reason": { "type": [ "string", "null" ] } }, "required": [ "index", "message", "finish_reason" ], "additionalProperties": {} } }, "usage": { "$ref": "#/components/schemas/Usage" } }, "required": [ "id", "object", "created", "model", "choices", "usage" ], "additionalProperties": {} }, "Usage": { "type": "object", "properties": { "prompt_tokens": { "type": "integer" }, "completion_tokens": { "type": "integer" }, "total_tokens": { "type": "integer" }, "cost": { "type": "number", "description": "What this request was charged, in US dollars. Always 0 with a test key.", "example": 0.00042 } }, "required": [ "prompt_tokens", "completion_tokens", "cost" ], "additionalProperties": {} }, "ChatCompletionRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "A model ID from `GET /v1/ai/models`.", "example": "anthropic/claude-sonnet-5" }, "messages": { "type": "array", "items": { "$ref": "#/components/schemas/ChatMessage" }, "minItems": 1 }, "stream": { "type": "boolean", "description": "Send the reply as server-sent events as it's written. The last event before `[DONE]` reports usage and cost." }, "max_completion_tokens": { "type": "integer", "exclusiveMinimum": 0, "description": "The longest reply to allow, in tokens. Without it, a team without auto top-up may get a lower limit that its credits cover." }, "max_tokens": { "type": "integer", "exclusiveMinimum": 0, "description": "Older name for `max_completion_tokens`." }, "temperature": { "type": "number" }, "tools": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "function" ] } }, "required": [ "type" ], "additionalProperties": {} }, "description": "Function tools. Other tool types are refused." }, "n": { "type": "integer", "description": "Only 1 is supported." } }, "required": [ "model", "messages" ], "additionalProperties": {}, "description": "OpenAI's Chat Completions format. Fields not listed here are passed to the model as sent, except `providerOptions` other than `gateway.order`, `gateway.only` and `gateway.sort`, and `user`, which are dropped." }, "ChatMessage": { "type": "object", "properties": { "role": { "type": "string", "minLength": 1, "example": "user" }, "content": { "description": "Text, or an array of text, image and file parts.", "example": "Hello" } }, "required": [ "role" ], "additionalProperties": {} }, "ImageGeneration": { "type": "object", "properties": { "created": { "type": "integer", "description": "Seconds since the epoch." }, "data": { "type": "array", "items": { "type": "object", "properties": { "b64_json": { "type": "string", "description": "The image, base64-encoded. Usually a PNG." } }, "required": [ "b64_json" ] } }, "usage": { "$ref": "#/components/schemas/ImageUsage" } }, "required": [ "created", "data", "usage" ] }, "ImageUsage": { "type": "object", "properties": { "images": { "type": "integer", "description": "Images made." }, "input_tokens": { "type": "integer", "description": "For models priced by tokens." }, "output_tokens": { "type": "integer", "description": "For models priced by tokens." }, "total_tokens": { "type": "integer" }, "cost": { "type": "number", "description": "What this request was charged, in US dollars. Always 0 with a test key.", "example": 0.0528 } }, "required": [ "images", "cost" ] }, "ImageGenerationRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "An image model ID from `GET /v1/ai/models?type=image`.", "example": "bfl/flux-2-pro" }, "prompt": { "type": "string", "minLength": 1, "maxLength": 32000, "description": "What to draw.", "example": "A lighthouse on a cliff at dawn, watercolor" }, "n": { "type": "integer", "minimum": 1, "maximum": 4, "description": "How many images to make, up to 4. Defaults to 1. Each is charged." }, "size": { "type": "string", "pattern": "^\\d{2,5}x\\d{2,5}$", "description": "Width and height in pixels, such as `1024x1024`. Sizes each model supports vary.", "example": "1024x1024" }, "aspect_ratio": { "type": "string", "pattern": "^\\d{1,3}:\\d{1,3}$", "description": "Width to height, such as `16:9`, for models that take a ratio instead of a size.", "example": "16:9" }, "seed": { "type": "integer", "description": "Makes results repeatable, on models that support it." }, "response_format": { "type": "string", "enum": [ "b64_json" ], "description": "Only `b64_json`: images are returned as base64, never as links." } }, "required": [ "model", "prompt" ], "additionalProperties": {}, "description": "OpenAI's Images format. Other fields, such as `quality` or `style`, are ignored." }, "SpeechRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "A speech model ID from `GET /v1/ai/models?type=speech`.", "example": "openai/tts-1" }, "input": { "type": "string", "minLength": 1, "maxLength": 5000, "description": "The text to speak, up to 5,000 characters. Each character is charged.", "example": "Your order shipped this morning." }, "voice": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The voice to speak in. Each provider has its own, such as `alloy` for OpenAI. Without one, the model's default.", "example": "alloy" }, "response_format": { "type": "string", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "description": "The audio format. Models that can't make it answer in their own, so read the `Content-Type` header." }, "instructions": { "type": "string", "maxLength": 4096, "description": "How to speak, such as tone or pace, on models that support it." }, "speed": { "type": "number", "minimum": 0.25, "maximum": 4, "description": "From 0.25 to 4. Defaults to 1, on models that support it." }, "language": { "type": "string", "minLength": 2, "maxLength": 35, "description": "The language of the text, such as `en`, when it matters." } }, "required": [ "model", "input" ], "additionalProperties": {}, "description": "OpenAI's speech format. Other fields are ignored." } }, "parameters": {} }, "paths": { "/v1/ai/models": { "get": { "operationId": "listModels", "tags": [ "AI" ], "summary": "List models", "description": "Returns every model you can call, newest first, with what it costs.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "enum": [ "text", "image", "speech" ], "description": "Only models of this type." }, "required": false, "description": "Only models of this type.", "name": "type", "in": "query" } ], "responses": { "200": { "description": "The models.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModelList" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model list could not be read. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/chat/completions": { "post": { "operationId": "createChatCompletion", "tags": [ "AI" ], "summary": "Create a chat completion", "description": "Sends messages to a model and returns its reply, in OpenAI's Chat Completions format. With a test key the reply is canned and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatCompletionRequest" } } } }, "responses": { "200": { "description": "The model's reply, or with `stream: true` a stream of `chat.completion.chunk` events.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatCompletion" } }, "text/event-stream": { "schema": { "type": "string" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/images/generations": { "post": { "operationId": "createImage", "tags": [ "AI" ], "summary": "Create an image", "description": "Makes images from a prompt, in OpenAI's Images format. With a test key the image is a placeholder and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImageGenerationRequest" } } } }, "responses": { "200": { "description": "The images.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImageGeneration" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/audio/speech": { "post": { "operationId": "createSpeech", "tags": [ "AI" ], "summary": "Create speech", "description": "Speaks text aloud and returns the audio file. With a test key the audio is a second of silence, as WAV, and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpeechRequest" } } } }, "responses": { "200": { "description": "The audio. `Upsky-Cost` says what it was charged, in US dollars, and `Upsky-Characters` how many characters were.", "headers": { "Upsky-Cost": { "schema": { "type": "string", "example": "0.0165" }, "description": "What this request was charged, in US dollars." }, "Upsky-Characters": { "schema": { "type": "string", "example": "1000" }, "description": "Characters charged." } }, "content": { "audio/mpeg": { "schema": { "type": "string", "format": "binary" } }, "audio/wav": { "schema": { "type": "string", "format": "binary" } }, "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": {} } # Create a chat completion (/en/ai/reference/createChatCompletion) { "openapi": "3.2.0", "info": { "title": "Upsky AI API", "version": "1.0.0" }, "servers": [ { "url": "https://api.upsky.dev" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "Your API key, starting with upsky_test_ or upsky_live_." } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "invalid_request", "unauthorized", "insufficient_credits", "pro_required", "account_suspended", "forbidden", "not_found", "method_not_allowed", "conflict", "idempotency_key_reused", "request_in_progress", "retry_later", "payload_too_large", "unsupported_media_type", "rate_limited", "internal_error", "service_unavailable" ], "description": "A stable code to branch on.", "example": "unauthorized" }, "message": { "type": "string", "description": "What went wrong, for a person to read.", "example": "The API key is not valid or has been revoked." }, "request_id": { "type": "string", "description": "Include this when you contact support.", "example": "req_3f9a1c0b7d2e4a6f8b1c2d3e" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] }, "ModelList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ "list" ] }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Model" } } }, "required": [ "object", "data" ] }, "Model": { "type": "object", "properties": { "id": { "type": "string", "description": "Send this as `model`. Always `provider/model-name`.", "example": "anthropic/claude-sonnet-5" }, "object": { "type": "string", "enum": [ "model" ] }, "type": { "type": "string", "enum": [ "text", "image", "speech" ], "description": "Which endpoint calls it: chat completions, image generations or speech.", "example": "text" }, "created": { "type": "integer", "description": "Seconds since the epoch." }, "owned_by": { "type": "string", "example": "anthropic" }, "name": { "type": "string", "example": "Claude Sonnet 5" }, "context_window": { "type": [ "integer", "null" ], "description": "The most tokens the model reads, prompt and reply together. Null for image and speech models." }, "max_output_tokens": { "type": [ "integer", "null" ], "description": "The longest reply the model writes, in tokens. Null for image and speech models." }, "pricing": { "$ref": "#/components/schemas/ModelPricing" } }, "required": [ "id", "object", "type", "created", "owned_by", "name", "context_window", "max_output_tokens", "pricing" ] }, "ModelPricing": { "type": "object", "properties": { "input": { "type": "number", "description": "Per million prompt tokens.", "example": 3.3 }, "output": { "type": "number", "description": "Per million tokens the model writes.", "example": 16.5 }, "cached_input": { "type": [ "number", "null" ], "description": "Per million prompt tokens read from the cache.", "example": 0.33 }, "image": { "type": "number", "description": "Per image, at the model's default size.", "example": 0.0528 }, "image_sizes": { "type": "array", "items": { "type": "object", "properties": { "size": { "type": "string" }, "image": { "type": "number" } }, "required": [ "size", "image" ] }, "description": "Per image at each size the model lists, smallest first. A size in between costs what the next one up does." }, "characters": { "type": "number", "description": "Per million characters of input.", "example": 16.5 } }, "description": "US dollars, with the fields that apply to the model's type. Text models, and image models without a price per image, are priced per million tokens; some charge more for very long prompts. Image models are priced per image and speech models per character." }, "ChatCompletion": { "type": "object", "properties": { "id": { "type": "string" }, "object": { "type": "string", "enum": [ "chat.completion" ] }, "created": { "type": "integer" }, "model": { "type": "string" }, "choices": { "type": "array", "items": { "type": "object", "properties": { "index": { "type": "integer" }, "message": { "type": "object", "properties": { "role": { "type": "string" }, "content": { "type": [ "string", "null" ] } }, "required": [ "role", "content" ], "additionalProperties": {} }, "finish_reason": { "type": [ "string", "null" ] } }, "required": [ "index", "message", "finish_reason" ], "additionalProperties": {} } }, "usage": { "$ref": "#/components/schemas/Usage" } }, "required": [ "id", "object", "created", "model", "choices", "usage" ], "additionalProperties": {} }, "Usage": { "type": "object", "properties": { "prompt_tokens": { "type": "integer" }, "completion_tokens": { "type": "integer" }, "total_tokens": { "type": "integer" }, "cost": { "type": "number", "description": "What this request was charged, in US dollars. Always 0 with a test key.", "example": 0.00042 } }, "required": [ "prompt_tokens", "completion_tokens", "cost" ], "additionalProperties": {} }, "ChatCompletionRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "A model ID from `GET /v1/ai/models`.", "example": "anthropic/claude-sonnet-5" }, "messages": { "type": "array", "items": { "$ref": "#/components/schemas/ChatMessage" }, "minItems": 1 }, "stream": { "type": "boolean", "description": "Send the reply as server-sent events as it's written. The last event before `[DONE]` reports usage and cost." }, "max_completion_tokens": { "type": "integer", "exclusiveMinimum": 0, "description": "The longest reply to allow, in tokens. Without it, a team without auto top-up may get a lower limit that its credits cover." }, "max_tokens": { "type": "integer", "exclusiveMinimum": 0, "description": "Older name for `max_completion_tokens`." }, "temperature": { "type": "number" }, "tools": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "function" ] } }, "required": [ "type" ], "additionalProperties": {} }, "description": "Function tools. Other tool types are refused." }, "n": { "type": "integer", "description": "Only 1 is supported." } }, "required": [ "model", "messages" ], "additionalProperties": {}, "description": "OpenAI's Chat Completions format. Fields not listed here are passed to the model as sent, except `providerOptions` other than `gateway.order`, `gateway.only` and `gateway.sort`, and `user`, which are dropped." }, "ChatMessage": { "type": "object", "properties": { "role": { "type": "string", "minLength": 1, "example": "user" }, "content": { "description": "Text, or an array of text, image and file parts.", "example": "Hello" } }, "required": [ "role" ], "additionalProperties": {} }, "ImageGeneration": { "type": "object", "properties": { "created": { "type": "integer", "description": "Seconds since the epoch." }, "data": { "type": "array", "items": { "type": "object", "properties": { "b64_json": { "type": "string", "description": "The image, base64-encoded. Usually a PNG." } }, "required": [ "b64_json" ] } }, "usage": { "$ref": "#/components/schemas/ImageUsage" } }, "required": [ "created", "data", "usage" ] }, "ImageUsage": { "type": "object", "properties": { "images": { "type": "integer", "description": "Images made." }, "input_tokens": { "type": "integer", "description": "For models priced by tokens." }, "output_tokens": { "type": "integer", "description": "For models priced by tokens." }, "total_tokens": { "type": "integer" }, "cost": { "type": "number", "description": "What this request was charged, in US dollars. Always 0 with a test key.", "example": 0.0528 } }, "required": [ "images", "cost" ] }, "ImageGenerationRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "An image model ID from `GET /v1/ai/models?type=image`.", "example": "bfl/flux-2-pro" }, "prompt": { "type": "string", "minLength": 1, "maxLength": 32000, "description": "What to draw.", "example": "A lighthouse on a cliff at dawn, watercolor" }, "n": { "type": "integer", "minimum": 1, "maximum": 4, "description": "How many images to make, up to 4. Defaults to 1. Each is charged." }, "size": { "type": "string", "pattern": "^\\d{2,5}x\\d{2,5}$", "description": "Width and height in pixels, such as `1024x1024`. Sizes each model supports vary.", "example": "1024x1024" }, "aspect_ratio": { "type": "string", "pattern": "^\\d{1,3}:\\d{1,3}$", "description": "Width to height, such as `16:9`, for models that take a ratio instead of a size.", "example": "16:9" }, "seed": { "type": "integer", "description": "Makes results repeatable, on models that support it." }, "response_format": { "type": "string", "enum": [ "b64_json" ], "description": "Only `b64_json`: images are returned as base64, never as links." } }, "required": [ "model", "prompt" ], "additionalProperties": {}, "description": "OpenAI's Images format. Other fields, such as `quality` or `style`, are ignored." }, "SpeechRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "A speech model ID from `GET /v1/ai/models?type=speech`.", "example": "openai/tts-1" }, "input": { "type": "string", "minLength": 1, "maxLength": 5000, "description": "The text to speak, up to 5,000 characters. Each character is charged.", "example": "Your order shipped this morning." }, "voice": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The voice to speak in. Each provider has its own, such as `alloy` for OpenAI. Without one, the model's default.", "example": "alloy" }, "response_format": { "type": "string", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "description": "The audio format. Models that can't make it answer in their own, so read the `Content-Type` header." }, "instructions": { "type": "string", "maxLength": 4096, "description": "How to speak, such as tone or pace, on models that support it." }, "speed": { "type": "number", "minimum": 0.25, "maximum": 4, "description": "From 0.25 to 4. Defaults to 1, on models that support it." }, "language": { "type": "string", "minLength": 2, "maxLength": 35, "description": "The language of the text, such as `en`, when it matters." } }, "required": [ "model", "input" ], "additionalProperties": {}, "description": "OpenAI's speech format. Other fields are ignored." } }, "parameters": {} }, "paths": { "/v1/ai/models": { "get": { "operationId": "listModels", "tags": [ "AI" ], "summary": "List models", "description": "Returns every model you can call, newest first, with what it costs.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "enum": [ "text", "image", "speech" ], "description": "Only models of this type." }, "required": false, "description": "Only models of this type.", "name": "type", "in": "query" } ], "responses": { "200": { "description": "The models.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModelList" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model list could not be read. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/chat/completions": { "post": { "operationId": "createChatCompletion", "tags": [ "AI" ], "summary": "Create a chat completion", "description": "Sends messages to a model and returns its reply, in OpenAI's Chat Completions format. With a test key the reply is canned and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatCompletionRequest" } } } }, "responses": { "200": { "description": "The model's reply, or with `stream: true` a stream of `chat.completion.chunk` events.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatCompletion" } }, "text/event-stream": { "schema": { "type": "string" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/images/generations": { "post": { "operationId": "createImage", "tags": [ "AI" ], "summary": "Create an image", "description": "Makes images from a prompt, in OpenAI's Images format. With a test key the image is a placeholder and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImageGenerationRequest" } } } }, "responses": { "200": { "description": "The images.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImageGeneration" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/audio/speech": { "post": { "operationId": "createSpeech", "tags": [ "AI" ], "summary": "Create speech", "description": "Speaks text aloud and returns the audio file. With a test key the audio is a second of silence, as WAV, and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpeechRequest" } } } }, "responses": { "200": { "description": "The audio. `Upsky-Cost` says what it was charged, in US dollars, and `Upsky-Characters` how many characters were.", "headers": { "Upsky-Cost": { "schema": { "type": "string", "example": "0.0165" }, "description": "What this request was charged, in US dollars." }, "Upsky-Characters": { "schema": { "type": "string", "example": "1000" }, "description": "Characters charged." } }, "content": { "audio/mpeg": { "schema": { "type": "string", "format": "binary" } }, "audio/wav": { "schema": { "type": "string", "format": "binary" } }, "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": {} } # Create an image (/en/ai/reference/createImage) { "openapi": "3.2.0", "info": { "title": "Upsky AI API", "version": "1.0.0" }, "servers": [ { "url": "https://api.upsky.dev" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "Your API key, starting with upsky_test_ or upsky_live_." } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "invalid_request", "unauthorized", "insufficient_credits", "pro_required", "account_suspended", "forbidden", "not_found", "method_not_allowed", "conflict", "idempotency_key_reused", "request_in_progress", "retry_later", "payload_too_large", "unsupported_media_type", "rate_limited", "internal_error", "service_unavailable" ], "description": "A stable code to branch on.", "example": "unauthorized" }, "message": { "type": "string", "description": "What went wrong, for a person to read.", "example": "The API key is not valid or has been revoked." }, "request_id": { "type": "string", "description": "Include this when you contact support.", "example": "req_3f9a1c0b7d2e4a6f8b1c2d3e" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] }, "ModelList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ "list" ] }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Model" } } }, "required": [ "object", "data" ] }, "Model": { "type": "object", "properties": { "id": { "type": "string", "description": "Send this as `model`. Always `provider/model-name`.", "example": "anthropic/claude-sonnet-5" }, "object": { "type": "string", "enum": [ "model" ] }, "type": { "type": "string", "enum": [ "text", "image", "speech" ], "description": "Which endpoint calls it: chat completions, image generations or speech.", "example": "text" }, "created": { "type": "integer", "description": "Seconds since the epoch." }, "owned_by": { "type": "string", "example": "anthropic" }, "name": { "type": "string", "example": "Claude Sonnet 5" }, "context_window": { "type": [ "integer", "null" ], "description": "The most tokens the model reads, prompt and reply together. Null for image and speech models." }, "max_output_tokens": { "type": [ "integer", "null" ], "description": "The longest reply the model writes, in tokens. Null for image and speech models." }, "pricing": { "$ref": "#/components/schemas/ModelPricing" } }, "required": [ "id", "object", "type", "created", "owned_by", "name", "context_window", "max_output_tokens", "pricing" ] }, "ModelPricing": { "type": "object", "properties": { "input": { "type": "number", "description": "Per million prompt tokens.", "example": 3.3 }, "output": { "type": "number", "description": "Per million tokens the model writes.", "example": 16.5 }, "cached_input": { "type": [ "number", "null" ], "description": "Per million prompt tokens read from the cache.", "example": 0.33 }, "image": { "type": "number", "description": "Per image, at the model's default size.", "example": 0.0528 }, "image_sizes": { "type": "array", "items": { "type": "object", "properties": { "size": { "type": "string" }, "image": { "type": "number" } }, "required": [ "size", "image" ] }, "description": "Per image at each size the model lists, smallest first. A size in between costs what the next one up does." }, "characters": { "type": "number", "description": "Per million characters of input.", "example": 16.5 } }, "description": "US dollars, with the fields that apply to the model's type. Text models, and image models without a price per image, are priced per million tokens; some charge more for very long prompts. Image models are priced per image and speech models per character." }, "ChatCompletion": { "type": "object", "properties": { "id": { "type": "string" }, "object": { "type": "string", "enum": [ "chat.completion" ] }, "created": { "type": "integer" }, "model": { "type": "string" }, "choices": { "type": "array", "items": { "type": "object", "properties": { "index": { "type": "integer" }, "message": { "type": "object", "properties": { "role": { "type": "string" }, "content": { "type": [ "string", "null" ] } }, "required": [ "role", "content" ], "additionalProperties": {} }, "finish_reason": { "type": [ "string", "null" ] } }, "required": [ "index", "message", "finish_reason" ], "additionalProperties": {} } }, "usage": { "$ref": "#/components/schemas/Usage" } }, "required": [ "id", "object", "created", "model", "choices", "usage" ], "additionalProperties": {} }, "Usage": { "type": "object", "properties": { "prompt_tokens": { "type": "integer" }, "completion_tokens": { "type": "integer" }, "total_tokens": { "type": "integer" }, "cost": { "type": "number", "description": "What this request was charged, in US dollars. Always 0 with a test key.", "example": 0.00042 } }, "required": [ "prompt_tokens", "completion_tokens", "cost" ], "additionalProperties": {} }, "ChatCompletionRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "A model ID from `GET /v1/ai/models`.", "example": "anthropic/claude-sonnet-5" }, "messages": { "type": "array", "items": { "$ref": "#/components/schemas/ChatMessage" }, "minItems": 1 }, "stream": { "type": "boolean", "description": "Send the reply as server-sent events as it's written. The last event before `[DONE]` reports usage and cost." }, "max_completion_tokens": { "type": "integer", "exclusiveMinimum": 0, "description": "The longest reply to allow, in tokens. Without it, a team without auto top-up may get a lower limit that its credits cover." }, "max_tokens": { "type": "integer", "exclusiveMinimum": 0, "description": "Older name for `max_completion_tokens`." }, "temperature": { "type": "number" }, "tools": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "function" ] } }, "required": [ "type" ], "additionalProperties": {} }, "description": "Function tools. Other tool types are refused." }, "n": { "type": "integer", "description": "Only 1 is supported." } }, "required": [ "model", "messages" ], "additionalProperties": {}, "description": "OpenAI's Chat Completions format. Fields not listed here are passed to the model as sent, except `providerOptions` other than `gateway.order`, `gateway.only` and `gateway.sort`, and `user`, which are dropped." }, "ChatMessage": { "type": "object", "properties": { "role": { "type": "string", "minLength": 1, "example": "user" }, "content": { "description": "Text, or an array of text, image and file parts.", "example": "Hello" } }, "required": [ "role" ], "additionalProperties": {} }, "ImageGeneration": { "type": "object", "properties": { "created": { "type": "integer", "description": "Seconds since the epoch." }, "data": { "type": "array", "items": { "type": "object", "properties": { "b64_json": { "type": "string", "description": "The image, base64-encoded. Usually a PNG." } }, "required": [ "b64_json" ] } }, "usage": { "$ref": "#/components/schemas/ImageUsage" } }, "required": [ "created", "data", "usage" ] }, "ImageUsage": { "type": "object", "properties": { "images": { "type": "integer", "description": "Images made." }, "input_tokens": { "type": "integer", "description": "For models priced by tokens." }, "output_tokens": { "type": "integer", "description": "For models priced by tokens." }, "total_tokens": { "type": "integer" }, "cost": { "type": "number", "description": "What this request was charged, in US dollars. Always 0 with a test key.", "example": 0.0528 } }, "required": [ "images", "cost" ] }, "ImageGenerationRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "An image model ID from `GET /v1/ai/models?type=image`.", "example": "bfl/flux-2-pro" }, "prompt": { "type": "string", "minLength": 1, "maxLength": 32000, "description": "What to draw.", "example": "A lighthouse on a cliff at dawn, watercolor" }, "n": { "type": "integer", "minimum": 1, "maximum": 4, "description": "How many images to make, up to 4. Defaults to 1. Each is charged." }, "size": { "type": "string", "pattern": "^\\d{2,5}x\\d{2,5}$", "description": "Width and height in pixels, such as `1024x1024`. Sizes each model supports vary.", "example": "1024x1024" }, "aspect_ratio": { "type": "string", "pattern": "^\\d{1,3}:\\d{1,3}$", "description": "Width to height, such as `16:9`, for models that take a ratio instead of a size.", "example": "16:9" }, "seed": { "type": "integer", "description": "Makes results repeatable, on models that support it." }, "response_format": { "type": "string", "enum": [ "b64_json" ], "description": "Only `b64_json`: images are returned as base64, never as links." } }, "required": [ "model", "prompt" ], "additionalProperties": {}, "description": "OpenAI's Images format. Other fields, such as `quality` or `style`, are ignored." }, "SpeechRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "A speech model ID from `GET /v1/ai/models?type=speech`.", "example": "openai/tts-1" }, "input": { "type": "string", "minLength": 1, "maxLength": 5000, "description": "The text to speak, up to 5,000 characters. Each character is charged.", "example": "Your order shipped this morning." }, "voice": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The voice to speak in. Each provider has its own, such as `alloy` for OpenAI. Without one, the model's default.", "example": "alloy" }, "response_format": { "type": "string", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "description": "The audio format. Models that can't make it answer in their own, so read the `Content-Type` header." }, "instructions": { "type": "string", "maxLength": 4096, "description": "How to speak, such as tone or pace, on models that support it." }, "speed": { "type": "number", "minimum": 0.25, "maximum": 4, "description": "From 0.25 to 4. Defaults to 1, on models that support it." }, "language": { "type": "string", "minLength": 2, "maxLength": 35, "description": "The language of the text, such as `en`, when it matters." } }, "required": [ "model", "input" ], "additionalProperties": {}, "description": "OpenAI's speech format. Other fields are ignored." } }, "parameters": {} }, "paths": { "/v1/ai/models": { "get": { "operationId": "listModels", "tags": [ "AI" ], "summary": "List models", "description": "Returns every model you can call, newest first, with what it costs.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "enum": [ "text", "image", "speech" ], "description": "Only models of this type." }, "required": false, "description": "Only models of this type.", "name": "type", "in": "query" } ], "responses": { "200": { "description": "The models.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModelList" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model list could not be read. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/chat/completions": { "post": { "operationId": "createChatCompletion", "tags": [ "AI" ], "summary": "Create a chat completion", "description": "Sends messages to a model and returns its reply, in OpenAI's Chat Completions format. With a test key the reply is canned and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatCompletionRequest" } } } }, "responses": { "200": { "description": "The model's reply, or with `stream: true` a stream of `chat.completion.chunk` events.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatCompletion" } }, "text/event-stream": { "schema": { "type": "string" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/images/generations": { "post": { "operationId": "createImage", "tags": [ "AI" ], "summary": "Create an image", "description": "Makes images from a prompt, in OpenAI's Images format. With a test key the image is a placeholder and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImageGenerationRequest" } } } }, "responses": { "200": { "description": "The images.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImageGeneration" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/audio/speech": { "post": { "operationId": "createSpeech", "tags": [ "AI" ], "summary": "Create speech", "description": "Speaks text aloud and returns the audio file. With a test key the audio is a second of silence, as WAV, and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpeechRequest" } } } }, "responses": { "200": { "description": "The audio. `Upsky-Cost` says what it was charged, in US dollars, and `Upsky-Characters` how many characters were.", "headers": { "Upsky-Cost": { "schema": { "type": "string", "example": "0.0165" }, "description": "What this request was charged, in US dollars." }, "Upsky-Characters": { "schema": { "type": "string", "example": "1000" }, "description": "Characters charged." } }, "content": { "audio/mpeg": { "schema": { "type": "string", "format": "binary" } }, "audio/wav": { "schema": { "type": "string", "format": "binary" } }, "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": {} } # Create speech (/en/ai/reference/createSpeech) { "openapi": "3.2.0", "info": { "title": "Upsky AI API", "version": "1.0.0" }, "servers": [ { "url": "https://api.upsky.dev" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "Your API key, starting with upsky_test_ or upsky_live_." } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "invalid_request", "unauthorized", "insufficient_credits", "pro_required", "account_suspended", "forbidden", "not_found", "method_not_allowed", "conflict", "idempotency_key_reused", "request_in_progress", "retry_later", "payload_too_large", "unsupported_media_type", "rate_limited", "internal_error", "service_unavailable" ], "description": "A stable code to branch on.", "example": "unauthorized" }, "message": { "type": "string", "description": "What went wrong, for a person to read.", "example": "The API key is not valid or has been revoked." }, "request_id": { "type": "string", "description": "Include this when you contact support.", "example": "req_3f9a1c0b7d2e4a6f8b1c2d3e" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] }, "ModelList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ "list" ] }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Model" } } }, "required": [ "object", "data" ] }, "Model": { "type": "object", "properties": { "id": { "type": "string", "description": "Send this as `model`. Always `provider/model-name`.", "example": "anthropic/claude-sonnet-5" }, "object": { "type": "string", "enum": [ "model" ] }, "type": { "type": "string", "enum": [ "text", "image", "speech" ], "description": "Which endpoint calls it: chat completions, image generations or speech.", "example": "text" }, "created": { "type": "integer", "description": "Seconds since the epoch." }, "owned_by": { "type": "string", "example": "anthropic" }, "name": { "type": "string", "example": "Claude Sonnet 5" }, "context_window": { "type": [ "integer", "null" ], "description": "The most tokens the model reads, prompt and reply together. Null for image and speech models." }, "max_output_tokens": { "type": [ "integer", "null" ], "description": "The longest reply the model writes, in tokens. Null for image and speech models." }, "pricing": { "$ref": "#/components/schemas/ModelPricing" } }, "required": [ "id", "object", "type", "created", "owned_by", "name", "context_window", "max_output_tokens", "pricing" ] }, "ModelPricing": { "type": "object", "properties": { "input": { "type": "number", "description": "Per million prompt tokens.", "example": 3.3 }, "output": { "type": "number", "description": "Per million tokens the model writes.", "example": 16.5 }, "cached_input": { "type": [ "number", "null" ], "description": "Per million prompt tokens read from the cache.", "example": 0.33 }, "image": { "type": "number", "description": "Per image, at the model's default size.", "example": 0.0528 }, "image_sizes": { "type": "array", "items": { "type": "object", "properties": { "size": { "type": "string" }, "image": { "type": "number" } }, "required": [ "size", "image" ] }, "description": "Per image at each size the model lists, smallest first. A size in between costs what the next one up does." }, "characters": { "type": "number", "description": "Per million characters of input.", "example": 16.5 } }, "description": "US dollars, with the fields that apply to the model's type. Text models, and image models without a price per image, are priced per million tokens; some charge more for very long prompts. Image models are priced per image and speech models per character." }, "ChatCompletion": { "type": "object", "properties": { "id": { "type": "string" }, "object": { "type": "string", "enum": [ "chat.completion" ] }, "created": { "type": "integer" }, "model": { "type": "string" }, "choices": { "type": "array", "items": { "type": "object", "properties": { "index": { "type": "integer" }, "message": { "type": "object", "properties": { "role": { "type": "string" }, "content": { "type": [ "string", "null" ] } }, "required": [ "role", "content" ], "additionalProperties": {} }, "finish_reason": { "type": [ "string", "null" ] } }, "required": [ "index", "message", "finish_reason" ], "additionalProperties": {} } }, "usage": { "$ref": "#/components/schemas/Usage" } }, "required": [ "id", "object", "created", "model", "choices", "usage" ], "additionalProperties": {} }, "Usage": { "type": "object", "properties": { "prompt_tokens": { "type": "integer" }, "completion_tokens": { "type": "integer" }, "total_tokens": { "type": "integer" }, "cost": { "type": "number", "description": "What this request was charged, in US dollars. Always 0 with a test key.", "example": 0.00042 } }, "required": [ "prompt_tokens", "completion_tokens", "cost" ], "additionalProperties": {} }, "ChatCompletionRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "A model ID from `GET /v1/ai/models`.", "example": "anthropic/claude-sonnet-5" }, "messages": { "type": "array", "items": { "$ref": "#/components/schemas/ChatMessage" }, "minItems": 1 }, "stream": { "type": "boolean", "description": "Send the reply as server-sent events as it's written. The last event before `[DONE]` reports usage and cost." }, "max_completion_tokens": { "type": "integer", "exclusiveMinimum": 0, "description": "The longest reply to allow, in tokens. Without it, a team without auto top-up may get a lower limit that its credits cover." }, "max_tokens": { "type": "integer", "exclusiveMinimum": 0, "description": "Older name for `max_completion_tokens`." }, "temperature": { "type": "number" }, "tools": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "function" ] } }, "required": [ "type" ], "additionalProperties": {} }, "description": "Function tools. Other tool types are refused." }, "n": { "type": "integer", "description": "Only 1 is supported." } }, "required": [ "model", "messages" ], "additionalProperties": {}, "description": "OpenAI's Chat Completions format. Fields not listed here are passed to the model as sent, except `providerOptions` other than `gateway.order`, `gateway.only` and `gateway.sort`, and `user`, which are dropped." }, "ChatMessage": { "type": "object", "properties": { "role": { "type": "string", "minLength": 1, "example": "user" }, "content": { "description": "Text, or an array of text, image and file parts.", "example": "Hello" } }, "required": [ "role" ], "additionalProperties": {} }, "ImageGeneration": { "type": "object", "properties": { "created": { "type": "integer", "description": "Seconds since the epoch." }, "data": { "type": "array", "items": { "type": "object", "properties": { "b64_json": { "type": "string", "description": "The image, base64-encoded. Usually a PNG." } }, "required": [ "b64_json" ] } }, "usage": { "$ref": "#/components/schemas/ImageUsage" } }, "required": [ "created", "data", "usage" ] }, "ImageUsage": { "type": "object", "properties": { "images": { "type": "integer", "description": "Images made." }, "input_tokens": { "type": "integer", "description": "For models priced by tokens." }, "output_tokens": { "type": "integer", "description": "For models priced by tokens." }, "total_tokens": { "type": "integer" }, "cost": { "type": "number", "description": "What this request was charged, in US dollars. Always 0 with a test key.", "example": 0.0528 } }, "required": [ "images", "cost" ] }, "ImageGenerationRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "An image model ID from `GET /v1/ai/models?type=image`.", "example": "bfl/flux-2-pro" }, "prompt": { "type": "string", "minLength": 1, "maxLength": 32000, "description": "What to draw.", "example": "A lighthouse on a cliff at dawn, watercolor" }, "n": { "type": "integer", "minimum": 1, "maximum": 4, "description": "How many images to make, up to 4. Defaults to 1. Each is charged." }, "size": { "type": "string", "pattern": "^\\d{2,5}x\\d{2,5}$", "description": "Width and height in pixels, such as `1024x1024`. Sizes each model supports vary.", "example": "1024x1024" }, "aspect_ratio": { "type": "string", "pattern": "^\\d{1,3}:\\d{1,3}$", "description": "Width to height, such as `16:9`, for models that take a ratio instead of a size.", "example": "16:9" }, "seed": { "type": "integer", "description": "Makes results repeatable, on models that support it." }, "response_format": { "type": "string", "enum": [ "b64_json" ], "description": "Only `b64_json`: images are returned as base64, never as links." } }, "required": [ "model", "prompt" ], "additionalProperties": {}, "description": "OpenAI's Images format. Other fields, such as `quality` or `style`, are ignored." }, "SpeechRequest": { "type": "object", "properties": { "model": { "type": "string", "minLength": 1, "maxLength": 200, "description": "A speech model ID from `GET /v1/ai/models?type=speech`.", "example": "openai/tts-1" }, "input": { "type": "string", "minLength": 1, "maxLength": 5000, "description": "The text to speak, up to 5,000 characters. Each character is charged.", "example": "Your order shipped this morning." }, "voice": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The voice to speak in. Each provider has its own, such as `alloy` for OpenAI. Without one, the model's default.", "example": "alloy" }, "response_format": { "type": "string", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "description": "The audio format. Models that can't make it answer in their own, so read the `Content-Type` header." }, "instructions": { "type": "string", "maxLength": 4096, "description": "How to speak, such as tone or pace, on models that support it." }, "speed": { "type": "number", "minimum": 0.25, "maximum": 4, "description": "From 0.25 to 4. Defaults to 1, on models that support it." }, "language": { "type": "string", "minLength": 2, "maxLength": 35, "description": "The language of the text, such as `en`, when it matters." } }, "required": [ "model", "input" ], "additionalProperties": {}, "description": "OpenAI's speech format. Other fields are ignored." } }, "parameters": {} }, "paths": { "/v1/ai/models": { "get": { "operationId": "listModels", "tags": [ "AI" ], "summary": "List models", "description": "Returns every model you can call, newest first, with what it costs.", "security": [ { "apiKey": [] } ], "parameters": [ { "schema": { "type": "string", "enum": [ "text", "image", "speech" ], "description": "Only models of this type." }, "required": false, "description": "Only models of this type.", "name": "type", "in": "query" } ], "responses": { "200": { "description": "The models.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModelList" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model list could not be read. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/chat/completions": { "post": { "operationId": "createChatCompletion", "tags": [ "AI" ], "summary": "Create a chat completion", "description": "Sends messages to a model and returns its reply, in OpenAI's Chat Completions format. With a test key the reply is canned and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatCompletionRequest" } } } }, "responses": { "200": { "description": "The model's reply, or with `stream: true` a stream of `chat.completion.chunk` events.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatCompletion" } }, "text/event-stream": { "schema": { "type": "string" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/images/generations": { "post": { "operationId": "createImage", "tags": [ "AI" ], "summary": "Create an image", "description": "Makes images from a prompt, in OpenAI's Images format. With a test key the image is a placeholder and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImageGenerationRequest" } } } }, "responses": { "200": { "description": "The images.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImageGeneration" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/ai/audio/speech": { "post": { "operationId": "createSpeech", "tags": [ "AI" ], "summary": "Create speech", "description": "Speaks text aloud and returns the audio file. With a test key the audio is a second of silence, as WAV, and nothing is charged.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpeechRequest" } } } }, "responses": { "200": { "description": "The audio. `Upsky-Cost` says what it was charged, in US dollars, and `Upsky-Characters` how many characters were.", "headers": { "Upsky-Cost": { "schema": { "type": "string", "example": "0.0165" }, "description": "What this request was charged, in US dollars." }, "Upsky-Characters": { "schema": { "type": "string", "example": "1000" }, "description": "Characters charged." } }, "content": { "audio/mpeg": { "schema": { "type": "string", "format": "binary" } }, "audio/wav": { "schema": { "type": "string", "format": "binary" } }, "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "The request isn't valid, or the model refused it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Not enough credits, and no automatic top-up could cover it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Live keys need an active Pro plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No such model.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "A top-up is being processed. Retry in a few seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "The model's provider failed. Retry with backoff.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": {} } # List files (/en/storage/reference/listFiles) { "openapi": "3.2.0", "info": { "title": "Upsky Storage API", "version": "0.1.0" }, "servers": [ { "url": "https://api.upsky.dev/v1" } ], "security": [ { "apiKey": [] } ], "paths": { "/storage/files": { "get": { "operationId": "listFiles", "summary": "List files", "description": "Returns your team's files, newest first.", "parameters": [ { "name": "cursor", "in": "query", "schema": { "type": "string" } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "maximum": 100 } } ], "responses": { "200": { "description": "A page of files.", "content": { "application/json": { "schema": { "type": "object", "required": [ "data", "next_cursor" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/File" } }, "next_cursor": { "type": [ "string", "null" ] } } } } } } } }, "post": { "operationId": "createFile", "summary": "Create a file", "description": "Reserves a file and returns a link to upload it to.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name", "content_type", "size" ], "properties": { "name": { "type": "string" }, "content_type": { "type": "string" }, "size": { "type": "integer", "description": "Size in bytes." } } } } } }, "responses": { "201": { "description": "The new file and its upload link.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/File" }, { "type": "object", "required": [ "upload_url" ], "properties": { "upload_url": { "type": "string", "format": "uri" } } } ] } } } }, "400": { "$ref": "#/components/responses/Error" } } } }, "/storage/files/{id}": { "get": { "operationId": "getFile", "summary": "Get a file", "description": "Returns a file with a short-lived download link.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "200": { "description": "The file.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/File" }, { "type": "object", "required": [ "download_url" ], "properties": { "download_url": { "type": "string", "format": "uri" } } } ] } } } }, "404": { "$ref": "#/components/responses/Error" } } }, "delete": { "operationId": "deleteFile", "summary": "Delete a file", "description": "Permanently deletes a file.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "204": { "description": "The file was deleted." }, "404": { "$ref": "#/components/responses/Error" } } } }, "/storage/files/{id}/complete": { "post": { "operationId": "completeFileUpload", "summary": "Complete an upload", "description": "Marks a file as uploaded once the client has finished sending it.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "200": { "description": "The uploaded file.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/File" } } } }, "404": { "$ref": "#/components/responses/Error" } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer" } }, "parameters": { "FileId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } }, "responses": { "Error": { "description": "The request failed.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message", "request_id" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } } } } } } } } }, "schemas": { "File": { "type": "object", "required": [ "id", "name", "content_type", "size", "status" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "content_type": { "type": "string" }, "size": { "type": "integer" }, "status": { "type": "string", "enum": [ "pending", "uploaded" ] } } } } } } # Create a file (/en/storage/reference/createFile) { "openapi": "3.2.0", "info": { "title": "Upsky Storage API", "version": "0.1.0" }, "servers": [ { "url": "https://api.upsky.dev/v1" } ], "security": [ { "apiKey": [] } ], "paths": { "/storage/files": { "get": { "operationId": "listFiles", "summary": "List files", "description": "Returns your team's files, newest first.", "parameters": [ { "name": "cursor", "in": "query", "schema": { "type": "string" } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "maximum": 100 } } ], "responses": { "200": { "description": "A page of files.", "content": { "application/json": { "schema": { "type": "object", "required": [ "data", "next_cursor" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/File" } }, "next_cursor": { "type": [ "string", "null" ] } } } } } } } }, "post": { "operationId": "createFile", "summary": "Create a file", "description": "Reserves a file and returns a link to upload it to.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name", "content_type", "size" ], "properties": { "name": { "type": "string" }, "content_type": { "type": "string" }, "size": { "type": "integer", "description": "Size in bytes." } } } } } }, "responses": { "201": { "description": "The new file and its upload link.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/File" }, { "type": "object", "required": [ "upload_url" ], "properties": { "upload_url": { "type": "string", "format": "uri" } } } ] } } } }, "400": { "$ref": "#/components/responses/Error" } } } }, "/storage/files/{id}": { "get": { "operationId": "getFile", "summary": "Get a file", "description": "Returns a file with a short-lived download link.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "200": { "description": "The file.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/File" }, { "type": "object", "required": [ "download_url" ], "properties": { "download_url": { "type": "string", "format": "uri" } } } ] } } } }, "404": { "$ref": "#/components/responses/Error" } } }, "delete": { "operationId": "deleteFile", "summary": "Delete a file", "description": "Permanently deletes a file.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "204": { "description": "The file was deleted." }, "404": { "$ref": "#/components/responses/Error" } } } }, "/storage/files/{id}/complete": { "post": { "operationId": "completeFileUpload", "summary": "Complete an upload", "description": "Marks a file as uploaded once the client has finished sending it.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "200": { "description": "The uploaded file.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/File" } } } }, "404": { "$ref": "#/components/responses/Error" } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer" } }, "parameters": { "FileId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } }, "responses": { "Error": { "description": "The request failed.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message", "request_id" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } } } } } } } } }, "schemas": { "File": { "type": "object", "required": [ "id", "name", "content_type", "size", "status" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "content_type": { "type": "string" }, "size": { "type": "integer" }, "status": { "type": "string", "enum": [ "pending", "uploaded" ] } } } } } } # Get a file (/en/storage/reference/getFile) { "openapi": "3.2.0", "info": { "title": "Upsky Storage API", "version": "0.1.0" }, "servers": [ { "url": "https://api.upsky.dev/v1" } ], "security": [ { "apiKey": [] } ], "paths": { "/storage/files": { "get": { "operationId": "listFiles", "summary": "List files", "description": "Returns your team's files, newest first.", "parameters": [ { "name": "cursor", "in": "query", "schema": { "type": "string" } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "maximum": 100 } } ], "responses": { "200": { "description": "A page of files.", "content": { "application/json": { "schema": { "type": "object", "required": [ "data", "next_cursor" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/File" } }, "next_cursor": { "type": [ "string", "null" ] } } } } } } } }, "post": { "operationId": "createFile", "summary": "Create a file", "description": "Reserves a file and returns a link to upload it to.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name", "content_type", "size" ], "properties": { "name": { "type": "string" }, "content_type": { "type": "string" }, "size": { "type": "integer", "description": "Size in bytes." } } } } } }, "responses": { "201": { "description": "The new file and its upload link.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/File" }, { "type": "object", "required": [ "upload_url" ], "properties": { "upload_url": { "type": "string", "format": "uri" } } } ] } } } }, "400": { "$ref": "#/components/responses/Error" } } } }, "/storage/files/{id}": { "get": { "operationId": "getFile", "summary": "Get a file", "description": "Returns a file with a short-lived download link.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "200": { "description": "The file.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/File" }, { "type": "object", "required": [ "download_url" ], "properties": { "download_url": { "type": "string", "format": "uri" } } } ] } } } }, "404": { "$ref": "#/components/responses/Error" } } }, "delete": { "operationId": "deleteFile", "summary": "Delete a file", "description": "Permanently deletes a file.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "204": { "description": "The file was deleted." }, "404": { "$ref": "#/components/responses/Error" } } } }, "/storage/files/{id}/complete": { "post": { "operationId": "completeFileUpload", "summary": "Complete an upload", "description": "Marks a file as uploaded once the client has finished sending it.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "200": { "description": "The uploaded file.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/File" } } } }, "404": { "$ref": "#/components/responses/Error" } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer" } }, "parameters": { "FileId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } }, "responses": { "Error": { "description": "The request failed.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message", "request_id" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } } } } } } } } }, "schemas": { "File": { "type": "object", "required": [ "id", "name", "content_type", "size", "status" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "content_type": { "type": "string" }, "size": { "type": "integer" }, "status": { "type": "string", "enum": [ "pending", "uploaded" ] } } } } } } # Delete a file (/en/storage/reference/deleteFile) { "openapi": "3.2.0", "info": { "title": "Upsky Storage API", "version": "0.1.0" }, "servers": [ { "url": "https://api.upsky.dev/v1" } ], "security": [ { "apiKey": [] } ], "paths": { "/storage/files": { "get": { "operationId": "listFiles", "summary": "List files", "description": "Returns your team's files, newest first.", "parameters": [ { "name": "cursor", "in": "query", "schema": { "type": "string" } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "maximum": 100 } } ], "responses": { "200": { "description": "A page of files.", "content": { "application/json": { "schema": { "type": "object", "required": [ "data", "next_cursor" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/File" } }, "next_cursor": { "type": [ "string", "null" ] } } } } } } } }, "post": { "operationId": "createFile", "summary": "Create a file", "description": "Reserves a file and returns a link to upload it to.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name", "content_type", "size" ], "properties": { "name": { "type": "string" }, "content_type": { "type": "string" }, "size": { "type": "integer", "description": "Size in bytes." } } } } } }, "responses": { "201": { "description": "The new file and its upload link.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/File" }, { "type": "object", "required": [ "upload_url" ], "properties": { "upload_url": { "type": "string", "format": "uri" } } } ] } } } }, "400": { "$ref": "#/components/responses/Error" } } } }, "/storage/files/{id}": { "get": { "operationId": "getFile", "summary": "Get a file", "description": "Returns a file with a short-lived download link.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "200": { "description": "The file.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/File" }, { "type": "object", "required": [ "download_url" ], "properties": { "download_url": { "type": "string", "format": "uri" } } } ] } } } }, "404": { "$ref": "#/components/responses/Error" } } }, "delete": { "operationId": "deleteFile", "summary": "Delete a file", "description": "Permanently deletes a file.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "204": { "description": "The file was deleted." }, "404": { "$ref": "#/components/responses/Error" } } } }, "/storage/files/{id}/complete": { "post": { "operationId": "completeFileUpload", "summary": "Complete an upload", "description": "Marks a file as uploaded once the client has finished sending it.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "200": { "description": "The uploaded file.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/File" } } } }, "404": { "$ref": "#/components/responses/Error" } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer" } }, "parameters": { "FileId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } }, "responses": { "Error": { "description": "The request failed.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message", "request_id" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } } } } } } } } }, "schemas": { "File": { "type": "object", "required": [ "id", "name", "content_type", "size", "status" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "content_type": { "type": "string" }, "size": { "type": "integer" }, "status": { "type": "string", "enum": [ "pending", "uploaded" ] } } } } } } # Complete an upload (/en/storage/reference/completeFileUpload) { "openapi": "3.2.0", "info": { "title": "Upsky Storage API", "version": "0.1.0" }, "servers": [ { "url": "https://api.upsky.dev/v1" } ], "security": [ { "apiKey": [] } ], "paths": { "/storage/files": { "get": { "operationId": "listFiles", "summary": "List files", "description": "Returns your team's files, newest first.", "parameters": [ { "name": "cursor", "in": "query", "schema": { "type": "string" } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "maximum": 100 } } ], "responses": { "200": { "description": "A page of files.", "content": { "application/json": { "schema": { "type": "object", "required": [ "data", "next_cursor" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/File" } }, "next_cursor": { "type": [ "string", "null" ] } } } } } } } }, "post": { "operationId": "createFile", "summary": "Create a file", "description": "Reserves a file and returns a link to upload it to.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name", "content_type", "size" ], "properties": { "name": { "type": "string" }, "content_type": { "type": "string" }, "size": { "type": "integer", "description": "Size in bytes." } } } } } }, "responses": { "201": { "description": "The new file and its upload link.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/File" }, { "type": "object", "required": [ "upload_url" ], "properties": { "upload_url": { "type": "string", "format": "uri" } } } ] } } } }, "400": { "$ref": "#/components/responses/Error" } } } }, "/storage/files/{id}": { "get": { "operationId": "getFile", "summary": "Get a file", "description": "Returns a file with a short-lived download link.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "200": { "description": "The file.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/File" }, { "type": "object", "required": [ "download_url" ], "properties": { "download_url": { "type": "string", "format": "uri" } } } ] } } } }, "404": { "$ref": "#/components/responses/Error" } } }, "delete": { "operationId": "deleteFile", "summary": "Delete a file", "description": "Permanently deletes a file.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "204": { "description": "The file was deleted." }, "404": { "$ref": "#/components/responses/Error" } } } }, "/storage/files/{id}/complete": { "post": { "operationId": "completeFileUpload", "summary": "Complete an upload", "description": "Marks a file as uploaded once the client has finished sending it.", "parameters": [ { "$ref": "#/components/parameters/FileId" } ], "responses": { "200": { "description": "The uploaded file.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/File" } } } }, "404": { "$ref": "#/components/responses/Error" } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer" } }, "parameters": { "FileId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } }, "responses": { "Error": { "description": "The request failed.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message", "request_id" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } } } } } } } } }, "schemas": { "File": { "type": "object", "required": [ "id", "name", "content_type", "size", "status" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "content_type": { "type": "string" }, "size": { "type": "integer" }, "status": { "type": "string", "enum": [ "pending", "uploaded" ] } } } } } } # List sites (/en/hosting/reference/listSites) { "openapi": "3.2.0", "info": { "title": "Upsky Hosting API", "version": "0.1.0" }, "servers": [ { "url": "https://api.upsky.dev/v1" } ], "security": [ { "apiKey": [] } ], "paths": { "/hosting/sites": { "get": { "operationId": "listSites", "summary": "List sites", "description": "Returns your team's sites.", "responses": { "200": { "description": "Your sites.", "content": { "application/json": { "schema": { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Site" } } } } } } } } }, "post": { "operationId": "createSite", "summary": "Create a site", "description": "Creates a new site.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name", "framework" ], "properties": { "name": { "type": "string" }, "framework": { "type": "string", "examples": [ "nextjs", "wordpress" ] } } } } } }, "responses": { "201": { "description": "The new site.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Site" } } } }, "400": { "$ref": "#/components/responses/Error" } } } }, "/hosting/sites/{id}/deployments": { "post": { "operationId": "createDeployment", "summary": "Create a deployment", "description": "Builds and launches a new version of a site.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "source" ], "properties": { "source": { "type": "object", "required": [ "type", "ref" ], "properties": { "type": { "type": "string", "enum": [ "git" ] }, "ref": { "type": "string", "examples": [ "main" ] } } } } } } } }, "responses": { "201": { "description": "The new deployment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Deployment" } } } }, "404": { "$ref": "#/components/responses/Error" } } } }, "/hosting/deployments/{id}": { "get": { "operationId": "getDeployment", "summary": "Get a deployment", "description": "Returns a deployment and its current status.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "The deployment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Deployment" } } } }, "404": { "$ref": "#/components/responses/Error" } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer" } }, "responses": { "Error": { "description": "The request failed.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message", "request_id" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } } } } } } } } }, "schemas": { "Site": { "type": "object", "required": [ "id", "name", "framework" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "framework": { "type": "string" } } }, "Deployment": { "type": "object", "required": [ "id", "site_id", "status" ], "properties": { "id": { "type": "string" }, "site_id": { "type": "string" }, "status": { "type": "string", "enum": [ "queued", "building", "ready", "failed" ] }, "url": { "type": [ "string", "null" ], "format": "uri" } } } } } } # Create a site (/en/hosting/reference/createSite) { "openapi": "3.2.0", "info": { "title": "Upsky Hosting API", "version": "0.1.0" }, "servers": [ { "url": "https://api.upsky.dev/v1" } ], "security": [ { "apiKey": [] } ], "paths": { "/hosting/sites": { "get": { "operationId": "listSites", "summary": "List sites", "description": "Returns your team's sites.", "responses": { "200": { "description": "Your sites.", "content": { "application/json": { "schema": { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Site" } } } } } } } } }, "post": { "operationId": "createSite", "summary": "Create a site", "description": "Creates a new site.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name", "framework" ], "properties": { "name": { "type": "string" }, "framework": { "type": "string", "examples": [ "nextjs", "wordpress" ] } } } } } }, "responses": { "201": { "description": "The new site.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Site" } } } }, "400": { "$ref": "#/components/responses/Error" } } } }, "/hosting/sites/{id}/deployments": { "post": { "operationId": "createDeployment", "summary": "Create a deployment", "description": "Builds and launches a new version of a site.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "source" ], "properties": { "source": { "type": "object", "required": [ "type", "ref" ], "properties": { "type": { "type": "string", "enum": [ "git" ] }, "ref": { "type": "string", "examples": [ "main" ] } } } } } } } }, "responses": { "201": { "description": "The new deployment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Deployment" } } } }, "404": { "$ref": "#/components/responses/Error" } } } }, "/hosting/deployments/{id}": { "get": { "operationId": "getDeployment", "summary": "Get a deployment", "description": "Returns a deployment and its current status.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "The deployment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Deployment" } } } }, "404": { "$ref": "#/components/responses/Error" } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer" } }, "responses": { "Error": { "description": "The request failed.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message", "request_id" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } } } } } } } } }, "schemas": { "Site": { "type": "object", "required": [ "id", "name", "framework" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "framework": { "type": "string" } } }, "Deployment": { "type": "object", "required": [ "id", "site_id", "status" ], "properties": { "id": { "type": "string" }, "site_id": { "type": "string" }, "status": { "type": "string", "enum": [ "queued", "building", "ready", "failed" ] }, "url": { "type": [ "string", "null" ], "format": "uri" } } } } } } # Create a deployment (/en/hosting/reference/createDeployment) { "openapi": "3.2.0", "info": { "title": "Upsky Hosting API", "version": "0.1.0" }, "servers": [ { "url": "https://api.upsky.dev/v1" } ], "security": [ { "apiKey": [] } ], "paths": { "/hosting/sites": { "get": { "operationId": "listSites", "summary": "List sites", "description": "Returns your team's sites.", "responses": { "200": { "description": "Your sites.", "content": { "application/json": { "schema": { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Site" } } } } } } } } }, "post": { "operationId": "createSite", "summary": "Create a site", "description": "Creates a new site.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name", "framework" ], "properties": { "name": { "type": "string" }, "framework": { "type": "string", "examples": [ "nextjs", "wordpress" ] } } } } } }, "responses": { "201": { "description": "The new site.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Site" } } } }, "400": { "$ref": "#/components/responses/Error" } } } }, "/hosting/sites/{id}/deployments": { "post": { "operationId": "createDeployment", "summary": "Create a deployment", "description": "Builds and launches a new version of a site.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "source" ], "properties": { "source": { "type": "object", "required": [ "type", "ref" ], "properties": { "type": { "type": "string", "enum": [ "git" ] }, "ref": { "type": "string", "examples": [ "main" ] } } } } } } } }, "responses": { "201": { "description": "The new deployment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Deployment" } } } }, "404": { "$ref": "#/components/responses/Error" } } } }, "/hosting/deployments/{id}": { "get": { "operationId": "getDeployment", "summary": "Get a deployment", "description": "Returns a deployment and its current status.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "The deployment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Deployment" } } } }, "404": { "$ref": "#/components/responses/Error" } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer" } }, "responses": { "Error": { "description": "The request failed.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message", "request_id" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } } } } } } } } }, "schemas": { "Site": { "type": "object", "required": [ "id", "name", "framework" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "framework": { "type": "string" } } }, "Deployment": { "type": "object", "required": [ "id", "site_id", "status" ], "properties": { "id": { "type": "string" }, "site_id": { "type": "string" }, "status": { "type": "string", "enum": [ "queued", "building", "ready", "failed" ] }, "url": { "type": [ "string", "null" ], "format": "uri" } } } } } } # Get a deployment (/en/hosting/reference/getDeployment) { "openapi": "3.2.0", "info": { "title": "Upsky Hosting API", "version": "0.1.0" }, "servers": [ { "url": "https://api.upsky.dev/v1" } ], "security": [ { "apiKey": [] } ], "paths": { "/hosting/sites": { "get": { "operationId": "listSites", "summary": "List sites", "description": "Returns your team's sites.", "responses": { "200": { "description": "Your sites.", "content": { "application/json": { "schema": { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Site" } } } } } } } } }, "post": { "operationId": "createSite", "summary": "Create a site", "description": "Creates a new site.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name", "framework" ], "properties": { "name": { "type": "string" }, "framework": { "type": "string", "examples": [ "nextjs", "wordpress" ] } } } } } }, "responses": { "201": { "description": "The new site.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Site" } } } }, "400": { "$ref": "#/components/responses/Error" } } } }, "/hosting/sites/{id}/deployments": { "post": { "operationId": "createDeployment", "summary": "Create a deployment", "description": "Builds and launches a new version of a site.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "source" ], "properties": { "source": { "type": "object", "required": [ "type", "ref" ], "properties": { "type": { "type": "string", "enum": [ "git" ] }, "ref": { "type": "string", "examples": [ "main" ] } } } } } } } }, "responses": { "201": { "description": "The new deployment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Deployment" } } } }, "404": { "$ref": "#/components/responses/Error" } } } }, "/hosting/deployments/{id}": { "get": { "operationId": "getDeployment", "summary": "Get a deployment", "description": "Returns a deployment and its current status.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "The deployment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Deployment" } } } }, "404": { "$ref": "#/components/responses/Error" } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer" } }, "responses": { "Error": { "description": "The request failed.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message", "request_id" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } } } } } } } } }, "schemas": { "Site": { "type": "object", "required": [ "id", "name", "framework" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "framework": { "type": "string" } } }, "Deployment": { "type": "object", "required": [ "id", "site_id", "status" ], "properties": { "id": { "type": "string" }, "site_id": { "type": "string" }, "status": { "type": "string", "enum": [ "queued", "building", "ready", "failed" ] }, "url": { "type": [ "string", "null" ], "format": "uri" } } } } } } # Get your team (/en/platform/reference/getTeam) { "openapi": "3.2.0", "info": { "title": "Upsky Platform API", "version": "1.0.0" }, "servers": [ { "url": "https://api.upsky.dev" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "Your API key, starting with upsky_test_ or upsky_live_." } }, "schemas": { "Team": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "The team's ID.", "example": "0b9f6a52-6d0c-4a3e-9d1e-3f4b2a7c8d90" }, "name": { "type": "string", "description": "The team's name.", "example": "Acme" }, "livemode": { "type": "boolean", "description": "Whether this request used a live key. Test keys never charge and never run real checks." } }, "required": [ "id", "name", "livemode" ] }, "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "enum": [ "invalid_request", "unauthorized", "insufficient_credits", "pro_required", "account_suspended", "forbidden", "not_found", "method_not_allowed", "conflict", "idempotency_key_reused", "request_in_progress", "retry_later", "payload_too_large", "unsupported_media_type", "rate_limited", "internal_error", "service_unavailable" ], "description": "A stable code to branch on.", "example": "unauthorized" }, "message": { "type": "string", "description": "What went wrong, for a person to read.", "example": "The API key is not valid or has been revoked." }, "request_id": { "type": "string", "description": "Include this when you contact support.", "example": "req_3f9a1c0b7d2e4a6f8b1c2d3e" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] } }, "parameters": {} }, "paths": { "/v1/platform/team": { "get": { "operationId": "getTeam", "tags": [ "Platform" ], "summary": "Get your team", "description": "Returns the team this API key belongs to.", "security": [ { "apiKey": [] } ], "responses": { "200": { "description": "The team.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Team" } } } }, "401": { "description": "The API key is missing, not valid or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Your team is over its rate limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": {} }