Your first AI API call
The AI applications API lets your own software run AI applications: you name an app, send the values of its input fields, and read the model's answer back once the run has finished. It is a JSON-over-HTTPS API. You authenticate with your AI project's key, call a path under /v1/ai/, and read the result from one response envelope.
Before you start — You need the API key of your AI project (it starts with
sk-project-). There is no self-service sign-up: the Ultineo team opens your project and hands you its first key. With that key you can then create and revoke your own keys, see AI API keys.
Base URL
Every request goes to:
https://api.ultineo.com/v1/ai/ Only HTTPS is served. A request on http:// is answered with a 301 redirect to the same path on https://.
Important — Your key travels in a request header, so a single plaintext request discloses it in full. Always call the
https://address directly rather than relying on the redirect.
One endpoint needs no key, and you can use it to check that the host is reachable:
curl https://api.ultineo.com/health {
"code": 200,
"title": "OK",
"response": "api gateway"
} Authenticate
Send your key as a Bearer token on every call:
| Header | Value |
|---|---|
Authorization | Bearer sk-project-… |
Content-Type | application/json (only when you send a body) |
A key belongs to one AI project. Every read and every write is limited to that project, whatever you put in the URL or the body: a record of another project answers exactly as a record that does not exist.
Only a hash of your key is stored, so a lost key cannot be recovered: revoke it and create a new one. A key can also be suspended or carry an expiry date; a suspended, revoked or expired key is refused (see AI API error codes).
Note — The AI API and the shop API described in Your first API call share the host
api.ultineo.combut not their keys. A shop key is refused on every/v1/ai/path, and an AI project key is refused on the shop paths.
Endpoints
Every path has the form /v1/ai/<resource>/<action>. An action that acts on one record takes its identifier as a final path segment, such as /v1/ai/run/detail/runi_….
| Method | Path | Scope | Topic |
|---|---|---|---|
| POST | /v1/ai/app/create | app:write | AI apps |
| GET | /v1/ai/app/list | app:read | AI apps |
| GET | /v1/ai/app/detail/<app_id> | app:read | AI apps |
| PATCH | /v1/ai/app/detail/<app_id> | app:write | AI apps |
| POST | /v1/ai/run/create | reasoning:write | AI runs |
| GET | /v1/ai/run/list | reasoning:read | AI runs |
| GET | /v1/ai/run/detail/<run_id> | reasoning:read | AI runs |
| GET | /v1/ai/run-usage/summary | usage:read | AI quota and token usage |
| POST | /v1/ai/key/create | key:write | AI API keys |
| GET | /v1/ai/key/list | key:read | AI API keys |
| GET | /v1/ai/key/detail/<key_id> | key:read | AI API keys |
| PATCH | /v1/ai/key/detail/<key_id> | key:write | AI API keys |
| DELETE | /v1/ai/key/detail/<key_id> | key:write | AI API keys |
Every identifier is a prefixed opaque string — mapp_ for an app, runi_ for a run, papi_ for a key — and you should treat it as an opaque value rather than parse it. Any other path answers 404.
Scopes
A key carries a list of scopes, and each endpoint requires one. The grammar has three forms:
| Scope | Grants |
|---|---|
* | Every action on every resource. |
<resource>:* | Every action on that one resource, such as reasoning:*. |
<resource>:<action> | One action on one resource, such as reasoning:read. |
Reading needs the :read action, and creating or editing needs :write. Call GET /v1/ai/key/detail/current to see the scopes your key holds (see AI API keys). A call that is short of its scope is refused with 403 and a message naming the missing scope.
Note — The run endpoints use the
reasoningscopes, notrun: a key scopedrun:*cannot launch or read runs.
Make the call
This launches a run of one of your apps:
curl -X POST https://api.ultineo.com/v1/ai/run/create \
-H "Authorization: Bearer sk-project-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"app_id": "mapp_0ceacb46e5b3a99e91b6",
"data": { "message": "My parcel has not arrived and the tracking has not moved for a week." }
}' {
"code": 200,
"title": "OK",
"response": {
"id": "runi_1b7f4e0c96a2d8531fca",
"object": "reasoning",
"data": { "message": "My parcel has not arrived and the tracking has not moved for a week." },
"instruction": { "1": "Classify the message as low, normal or urgent." },
"status": "new",
"created_at": 1789210200
}
} Runs are asynchronous. Read the run back with its id until its status is finished or failed:
curl https://api.ultineo.com/v1/ai/run/detail/runi_1b7f4e0c96a2d8531fca \
-H "Authorization: Bearer sk-project-YOUR_KEY" A finished run carries the model's answer in response, with its duration and token counts. See AI runs.
The response envelope
Every endpoint answers with the same three keys, on success and on failure alike. Read the HTTP status code first, then read response.
| Field | Type | Description |
|---|---|---|
code | integer | The HTTP status code, repeated in the body. |
title | string | The short status label, such as OK or Not Found. |
response | object or string | The result on success; an explanatory message on failure. |
A single record puts the object straight into response, with an object field naming its type. A list wraps its rows:
{
"code": 200,
"title": "OK",
"response": {
"object": "list",
"data": [],
"total": 0
}
} total counts every row that matches, not the number returned in data, so use it to drive pagination.
Pagination
The list endpoints accept two query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Rows to return, from 1 to 100. A value below 1 falls back to 50, and anything above 100 is capped at 100. |
offset | integer | 0 | Rows to skip. A negative value is treated as 0. |
Rows come newest first.
Dates and empty values
Every timestamp is an integer number of seconds since the Unix epoch, in UTC. A timestamp that has not happened yet is null rather than 0, so "expires_at": null means the key never expires. A text field that was never filled in is an empty string rather than null.
Request bodies
Send POST and PATCH bodies as a JSON object with Content-Type: application/json. A body that is not valid JSON, or that is not a JSON object, is refused with 400 and the message Invalid JSON. A body over 200,000 bytes is refused with 413.
A PATCH only changes the fields you send. Sending no recognised field at all is an error rather than a silent success, so a 200 means your change was applied.