# 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": {}
}