API HTTP¶
Wszystko, co robi konsola, robi przez to API. Nie ma prywatnej powierzchni: te same endpointy są dostępne dla ciebie.
Interaktywna referencja jest generowana z kodu i serwowana przez sam deployment
pod /docs, ze schematem pod /api/v1/openapi.json. Oba są włączone w
środowisku deweloperskim i wyłączone na produkcji — decyduje ENVIRONMENT, więc
produkcyjny deployment nie publikuje własnej listy tras.
Uwierzytelnianie¶
Trzy drogi wejścia, dla trzech różnych wywołujących.
| Nagłówek | Dla | |
|---|---|---|
| Klucz API organizacji | Authorization: Bearer aos_… |
Skryptu, klienta HTTP takiego jak Postman albo klienta MCP. Działa jako członek, który go wydał, zawężony do uprawnień, z którymi został wydany |
| JWT | Authorization: Bearer <access token> |
Osoby albo czegoś, co działa w jej imieniu. Krótko żyjący, odświeżany refresh tokenem |
| Ciasteczko sesji | ustawiane przez konsolę | Wyłącznie przeglądarki — token jest HttpOnly i nigdy nie trafia do JavaScriptu |
Klucze API organizacji¶
Klucz wydaje członek, w jednej organizacji, w Ustawienia → API (albo
przez POST /api/v1/api-keys z zalogowanej sesji). Niesie uprawnienia tego
członka, zawężone dwukrotnie:
- Do uprawnień, z którymi został wydany. Wybierz preset — Tylko odczyt, Zasilanie bazy wiedzy, Pełny dostęp — albo zaznacz uprawnienia z katalogu. Możesz nadać tylko to, co sam masz.
- Do tego, co wydający może zrobić teraz. Każde żądanie odczytuje członkostwo wydającego na nowo, więc jego degradacja od razu zawęża każdy z jego kluczy. Usunięcie go z organizacji albo jego odejście unieważnia wszystkie wydane przez niego klucze i podłączone aplikacje - na stałe, więc ponowne zaproszenie żadnego z nich nie przywraca. Grant na zasobie poszerza to, co może zrobić z jednym wierszem osoba; nigdy nie poszerza klucza ponad jego uprawnienia.
Klucz jest pokazywany raz, w odpowiedzi, która go tworzy. Przechowywany jest
wyłącznie jego SHA-256 i nigdy nie trafia do linii logu, wpisu audytu ani treści
błędu. Listy pokazują jego prefiks (aos_1a2b3c4d) — i tak samo wpis audytu
nazywa klucz, który zadziałał: każdy wpis zapisany w trakcie żądania
uwierzytelnionego kluczem niesie w szczegółach via_api_key. Klucz może mieć datę
wygaśnięcia, a jego unieważnienie (DELETE /api/v1/api-keys/{id}) działa od
następnego żądania.
Klucz nie jest osobą, która go wystawiła. Run, który uruchamia, należy do wystawcy, jest liczony w jego budżecie i audycie, ale nie sięga do niczego osobistego: ani do pamięci, ani do wyszukiwania jego dawnych rozmów, ani do stron, na których przeglądarka trzyma jego logowania, ani do przestrzeni roboczej sandboxa przypisanej do użytkownika. Ten, kto trzyma klucz, jest skryptem, nie tą osobą, więc run odpowiada tak jak na publicznym widgecie.
Odczyt połączeń jest też publiczny: GET /sandbox-connections,
GET /sandbox-connections/runtimes i GET /sandbox-connections/{id}/policy
wymagają connections:view; GET /mcp-connections wymaga mcp:manage.
Nadal obowiązują organizacja i zakresy klucza. Zapisy połączeń oraz operacje
na poświadczeniach pozostają dostępne tylko w sesji.
Warto znać dwie odmowy:
403"API keys are not accepted on this endpoint" — klucze są przyjmowane wyłącznie w publicznym API: agenci, runy i zatwierdzenia, bazy wiedzy i RAG, skille, pliki kontekstu, projekty, aplikacje, usługi ML,/me/permissionsoraz członkowie, zaproszenia, grupy i ustawienia organizacji. Własne trasy konsoli, twoje konto, opuszczenie lub przekazanie organizacji i samo zarządzanie kluczami pozostają tylko dla sesji, więc wyciekły klucz nie wybije swojego następcy.401"Invalid, expired or revoked API key" — to samo zdanie w każdym przypadku, więc zły klucz nie dowiaduje się niczego o tym, jakie klucze istnieją.
Każdy klucz ma też własny limit, RATE_LIMIT_API_KEY_PER_MINUTE żądań na minutę
(domyślnie 600), a run albo wywołanie ML, które wykona, liczy się do tych limitów
dla klucza, a nie dla jego wydającego. Oba sprawdzenia odbywają się przed
wyszukaniem klucza, w żądaniu i przy otwieraniu WebSocketu: klucz jest liczony po
skrócie całego tokenu - nigdy po prefiksie, który widać na listach - a adres,
który wysłał już RATE_LIMIT_AUTH_PER_MINUTE nieistniejących kluczy, dostaje
429 bez wyszukiwania do końca swojej minuty.
Bez przeglądarki¶
Skrypt może zdobyć własny klucz bez otwierania konsoli. Zaloguj się ciałem
formularza (OAuth2 password grant, więc JSON dostaje 422), wybierz organizację,
wydaj klucz tokenem sesji i tak samo go unieważnij, kiedy skończy:
SESSION=$(curl -s -X POST "$BASE/api/v1/auth/login" \
-d "username=$EMAIL" --data-urlencode "password=$PASSWORD" | jq -r .access_token)
ORG_ID=$(curl -s "$BASE/api/v1/orgs" -H "Authorization: Bearer $SESSION" \
| jq -r '[.items[] | select(.is_personal)][0].id')
curl -s -X POST "$BASE/api/v1/api-keys" \
-H "Authorization: Bearer $SESSION" -H "X-Organization-Id: $ORG_ID" \
-H "Content-Type: application/json" \
-d '{"name": "nightly import", "scopes": ["collections:view", "collections:edit"]}'
# -> {"id": "...", "key": "aos_...", "prefix": "aos_1a2b3c4d", ...}
curl -s -X DELETE "$BASE/api/v1/api-keys/$KEY_ID" \
-H "Authorization: Bearer $SESSION" -H "X-Organization-Id: $ORG_ID"
GET /api/v1/api-keys/scopes wymienia, co zalogowany członek może dać
kluczowi, i presety nad tym. Deployment, który loguje ludzi wyłącznie przez SSO,
nie ma hasła do wysłania; tam członek wydaje klucz w konsoli.
examples/public-api.sh to cała ta droga w jednym
skrypcie: wydaje klucz tylko do odczytu, listuje nim agentów i runy, pokazuje
403 dla trasy spoza uprawnień klucza, uruchamia agenta i czyta run, gdy
ustawione jest AGENT_ID, a na koniec unieważnia klucz.
Sesje i unieważnianie¶
Access token JWT jest związany z sesją, którą otworzyło logowanie — identyfikator
sesji podróżuje wewnątrz tokena. Wylogowanie się wszędzie (DELETE /sessions)
dezaktywuje te sesje, a związany token zostaje wtedy odrzucony przy następnym
użyciu, zamiast dożyć swoich kilku pozostałych minut. Sięga to również otwartego
WebSocketu czatu: następna ramka na unieważnionej sesji zamyka gniazdo, a nie
tylko następne żądanie HTTP.
Odświeżenie nie zaczyna nowej sesji — refresh token rotuje w miejscu, a access token dalej nazywa tę samą sesję — więc długo żyjące połączenie nie zostaje przerwane przez rutynowe odświeżenie.
Nagłówek organizacji¶
Klucz API działa we własnej organizacji i nie potrzebuje nagłówka. Wysłanie
X-Organization-Id razem z kluczem jest dozwolone tylko wtedy, gdy nazywa tę samą
organizację; wskazanie innej kończy się 400 z
details.header = "X-Organization-Id" zamiast zmiany najemcy.
Token sesji bierze najemcę z X-Organization-Id. Wywołujący, który należy do
trzech organizacji, jest w każdej z nich innym podmiotem, z inną rolą i innymi
grantami, więc wysyłaj nagłówek przy każdym żądaniu. Gdy go brakuje, żądanie
wraca do domyślnej organizacji wdrożenia, jeśli wywołujący do niej należy,
potem do jego osobistej organizacji, potem do jedynej, do której należy —
skrypt, który zapomni nagłówka, działa tam, z agentami, grantami i budżetem tej
organizacji, i nie dostaje żadnego błędu. Wywołujący, który należy do kilku i nie
ma żadnej z nich, dostaje 400 z details.header = "X-Organization-Id" zamiast
zgadywania (gdzie zaczyna nowe konto).
Wyślij zły, a dostaniesz odmowę, która wygląda dokładnie jak nieistniejący
zasób — celowo, żeby identyfikatorów nie dało się sondować.
Uruchamianie agenta¶
curl -X POST "$BASE/api/v1/agents/$AGENT_ID/run" \
-H "Authorization: Bearer $AGENTICOS_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "How do I rotate a provider key?"}'
Odpowiedź niesie identyfikator runa, wynik, status, tokeny i koszt.
environment_id w ciele wybiera, które środowisko odpowiada.
"draft": true uruchamia zamiast tego nieopublikowany draft, jak panel testowy
Buildera: run testowy, dla klucza, którego wystawca może edytować agenta,
odrzucany z podaniem powodu, gdy draft by się nie opublikował, i nigdy z
Prefer: respond-async.
Klucz sięga też do rutyn agenta - GET /api/v1/triggers oraz GET, POST,
PATCH i DELETE pod /api/v1/agents/{agent_id}/triggers, a także
POST .../{trigger_id}/run, żeby uruchomić jedną od razu - i do modeli organizacji
pod GET /api/v1/providers/model-profiles. Trzy ścieżki działają na samym
wystawcy: GET /api/v1/me/mcp-connections wymienia usługi, które połączył własnym
kontem, PUT /api/v1/users/me/locale ustawia mu język konsoli na każdym
urządzeniu, a PATCH /api/v1/users/me/agent-context zapisuje jego rolę
(job_title) i to, czym się zajmuje (about), co słyszy każdy agent. Każda
wymaga klucza wydanego na agents:run.
POST /api/v1/agents tworzy agenta ze speca w treści i zostawia puste pole
puste. Z "from_starter": true uzupełnia to, czego spec nie podaje -
instrukcje, skille, własne narzędzia platformy i budżet - z agenta startowego
AgenticOS, tak jak robi to New agent w konsoli.
Run kontynuuje rozmowę. Wywołanie bez conversation_id otwiera nową rozmowę
dla wystawcy klucza i zwraca jej identyfikator jako conversation_id; wyślij go
w kolejnym wywołaniu, a agent przeczyta, co padło wcześniej - ostatnie tury albo
podsumowanie starszych i to, co po nim. Workspace współdzielony per rozmowa
(domyślny) należy do tej rozmowy. Nieznany identyfikator albo cudza rozmowa
dostają 404; własna rozmowa wystawcy z konsoli albo kanału dostaje 400 -
klucz, który może uruchamiać agentów, nie dostał czatów tej osoby. Podobnie
z zaparkowanym przebiegiem: klucz wznawia albo odpowiada tylko na przebieg
rozpoczęty kluczem, a ten rozpoczęty przez osobę dostaje 403, bo kontynuacja
działa w jej imieniu. Rozmowa trafia na listę czatów wystawcy z etykietą API
i pozostaje ukryta, dopóki nie włączy tam takich rozmów przełącznikiem.
GET /api/v1/conversations/{id}/transcript czyta jedną z rozmów wystawcy tura po
turze, z narzędziami wywołanymi w każdej turze i kosztem, dla klucza z
runs:view.
Wywołujący API nie może obejść nadzoru
Ten endpoint przechodzi przez ten sam runner co konsola, Slack i widget. Run zostaje zapisany, budżet jest sprawdzany przed żądaniem do modelu, obowiązuje bramka zatwierdzeń, a koszt ląduje na tym samym dashboardzie.
Na tym polega jeden runner i dlatego nie ma "szybkiej ścieżki", która by go omijała.
Bez trzymania połączenia¶
Run trwający minuty - przeglądanie stron, długi łańcuch narzędzi - trzyma
żądanie otwarte przez cały czas, a limit czasu odczytu na proxy może je uciąć,
gdy run trwa dalej. Wyślij Prefer: respond-async, a wywołanie od razu
odpowie 202 z żądaniem runu do odpytywania:
curl -X POST "$BASE/api/v1/agents/$AGENT_ID/run" \
-H "Authorization: Bearer $AGENTICOS_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: respond-async" \
-H "Idempotency-Key: ticket-4711" \
-d '{"prompt": "Summarise this quarter for the board"}'
# 202, Location: /api/v1/run-requests/{id}, Preference-Applied: respond-async
curl "$BASE/api/v1/run-requests/$REQUEST_ID" -H "Authorization: Bearer $AGENTICOS_KEY"
Żądanie ma stan queued, running, a potem completed z result - tym samym
ciałem, którym odpowiada wywołanie synchroniczne - albo failed z error. Jego
conversation_id jest znane od pierwszej odpowiedzi, więc kolejną wiadomość
można wysłać, zanim run się skończy. Wykonuje je worker, przez ten sam runner,
budżet i zatwierdzenia co na każdej innej powierzchni.
To, co da się sprawdzić od razu, jest sprawdzane od razu: nieznana rozmowa albo
załącznik to 404 na POST, a nie błąd, na który trzeba czekać. Uprawnienia
klucza są sprawdzane ponownie, gdy run startuje, więc klucz odwołany w czasie,
gdy run czekał, go nie uruchomi. Żądanie runu może odczytać tylko osoba, która
o nie poprosiła, albo jej klucz; cudze odpowiada 404. Asynchroniczny run ma
jedną próbę - run, który się nie powiódł, jest zapisany z tym, co wydał, i nie
jest ponawiany, żeby nie wydać tego drugi raz.
Ta trasa niesie limit tempa, a nie bramkę uprawnień. Uprawnienie jest rozstrzygane wewnątrz serwisu, wobec grantów tego konkretnego agenta — bramka rolowa na trasie per zasób nie widzi ich.
PATCH /api/v1/agents/{id}/metadata ustawia categories i tags agenta
ciałem w rodzaju {"categories": [...], "tags": [...]}, gdzie pusta lista
czyści dany aspekt. Wartości są normalizowane — przycinane, ze scaloną spacją,
zwinięte wielkością liter i odduplikowane — oraz ograniczone: najwyżej 10
categories i 20 tagów, każdy najwyżej 32 znaki, a dłuższy element odpowiada
422. Podobnie jak trasa run, nie niesie bramki rolowej; rozstrzyga
uwzględniające granty sprawdzenie agents:edit wewnątrz serwisu, więc viewer z
grantem edycji na jednym agencie może go otagować.
GET /api/v1/agents filtruje ten katalog powtarzalnymi parametrami zapytania
category i tag: wartości łączą się przez OR w obrębie aspektu i AND
między aspektami, dopasowywane bez względu na wielkość liter (wartość zapytania
zwija się tak jak zapisana, a pusta wartość jest pomijana). Filtr tylko zawęża
to, co i tak już widzisz — nigdy nie przekracza granicy najemcy ani grantu.
Odpowiedź niesie też categories i tags: każdą odrębną etykietę na agentach,
które możesz wylistować, niezależnie od filtra i strony — wybory, które podaje menu
filtra. Prywatny agent, którego nie widzisz, nie dodaje żadnej.
Agenci przez A2A¶
Agent, którego organizacja udostępnia przez A2A, odpowiada na JSON-RPC pod
POST /api/v1/a2a/{agent_id}, z tymi samymi kluczami co /run; jego agent card jest
pod GET /api/v1/a2a/{agent_id}/.well-known/agent-card.json i nie wymaga żadnego.
PUT /api/v1/agents/{id}/a2a z {"enabled": true} włącza to wywołującemu, który ma
agents:publish na tym agencie.
curl -X POST "$BASE/api/v1/a2a/$AGENT_ID" \
-H "Authorization: Bearer $AGENTICOS_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "message/send",
"params": {"message": {"role": "user", "messageId": "m1",
"parts": [{"kind": "text", "text": "Review clause 7"}]}}}'
Odpowiedzią jest Task, którego id to id runa, a contextId kontynuuje rozmowę.
tasks/get odczytuje go ponownie. Resztę opisują Kanały.
Zdalnych agentów, do których deleguje ta organizacja, rejestruje się pod
/api/v1/external-agents: GET je listuje (agents:view); POST z card_url,
opcjonalnym slug i opcjonalnym secret_id klucza API odczytuje kartę i rejestruje
agenta, a POST /{id}/refresh odczytuje ją ponownie; DELETE /{id} usuwa
rejestrację (agents:edit dla wszystkich trzech).
Usługi ML¶
Cztery usługi platformy odpowiadają samodzielnie, bez rozmowy i bez agenta za
nimi: analiza dokumentu, OCR, zamiana mowy na tekst i wykrywanie danych
osobowych. Bramkuje je ml:invoke, a nie agents:run; ich dokumentacją są
Usługi ML.
curl -X POST "$BASE/api/v1/ml/privacy/pii" \
-H "Authorization: Bearer $AGENTICOS_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "write to ada@example.com"}'
Przykłady krok po kroku¶
Każdy działa z kluczem organizacji w $AGENTICOS_KEY i adresem API w $BASE.
Utwórz klucz w Ustawienia → API z uprawnieniami, które przykład
wymienia; konsola pokaże klucz raz.
Wgraj dokument do bazy wiedzy i przeszukaj ją — klucz z collections:view i
collections:edit (preset Zasilanie bazy wiedzy). Ingest działa w tle, więc
wyszukiwanie zaraz po wgraniu może jeszcze nie znaleźć dokumentu; jego status
pokazuje GET /api/v1/kb/$KB_ID/documents, a GET /api/v1/kb/$KB_ID/ingestion
odpowiada complete, gdy żaden nie jest już przetwarzany.
# Find the knowledge base, upload a file into it, and search it.
curl "$BASE/api/v1/kb" -H "Authorization: Bearer $AGENTICOS_KEY"
curl -X POST "$BASE/api/v1/kb/$KB_ID/documents" \
-H "Authorization: Bearer $AGENTICOS_KEY" \
-F "file=@policy.pdf"
curl -X POST "$BASE/api/v1/rag/search" \
-H "Authorization: Bearer $AGENTICOS_KEY" \
-H "Content-Type: application/json" \
-d "{\"collection_name\": \"$COLLECTION_NAME\", \"query\": \"refund window\"}"
Uruchom agenta i sprawdź, ile kosztował — agents:run i runs:view. Odczyt
runu niesie jego status, tokeny i koszt.
RUN_ID=$(curl -s -X POST "$BASE/api/v1/agents/$AGENT_ID/run" \
-H "Authorization: Bearer $AGENTICOS_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Summarise this week'"'"'s tickets"}' | jq -r .run_id)
curl "$BASE/api/v1/runs/$RUN_ID" -H "Authorization: Bearer $AGENTICOS_KEY"
Zaproś członka — members:manage. Zaproszenie idzie mailem; odpowiedź niesie
jego token raz, na wypadek gdyby mail do zapraszającego nie doszedł.
curl -X POST "$BASE/api/v1/orgs/$ORG_ID/invitations" \
-H "Authorization: Bearer $AGENTICOS_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "new.hire@example.com", "role": "member"}'
Ponawiaj tylko to, co trafiło na limit. 429 niesie Retry-After; każda inna
odmowa jest dla tego żądania ostateczna, a 401 znaczy, że klucza już nie ma —
unieważniony, wygasły albo jego wydający usunięty — więc ponawianie tylko zużywa
limit.
import time
import httpx
def call(client: httpx.Client, method: str, path: str, **kwargs) -> httpx.Response:
for _ in range(5):
response = client.request(method, path, **kwargs)
if response.status_code != 429:
response.raise_for_status()
return response
time.sleep(int(response.headers.get("Retry-After", "60")))
response.raise_for_status()
return response
client = httpx.Client(
base_url="https://agenticos.example.com/api/v1",
headers={"Authorization": f"Bearer {KEY}"},
)
print(call(client, "GET", "/me/permissions").json())
Streaming¶
Dwa endpointy WebSocket, dla dwóch odbiorców.
/api/v1/ws/agent— uwierzytelniony, którego używa konsola. Ramka niosącaagent_iduruchamia tego opublikowanego agenta; ramka bez niego trafia do ogólnego asystenta.conversation_idw ramce kontynuuje tę rozmowę;nullzaczyna nową, a brak tego klucza kontynuuje rozmowę, do której to gniazdo pisało ostatnio. Uwierzytelnij się subprotokołemaccess_token.<token>, gdzie token to JWT sesji. Klucz API organizacji jest tu odrzucany: tura na tym gnieździe to osoba przy klawiaturze, z jej osobistymi połączeniami. Integracje uruchamiają agentów przezPOST /api/v1/agents/{id}/run./api/v1/embed/{public_key}/ws— publiczny, stojący za embedem, dla odwiedzającego, który nie ma konta.
Oba strumieniują tokeny w miarę ich napływania (agent z guardrailem na wyjściu streamuje krok po kroku, zobacz Guardrails) i oba tworzą zwyczajny run, z tymi samymi księgami co wszystko inne.
Trzeci, /api/v1/ws/events, tylko nasłuchuje. Dzięki niemu otwarta konsola
nadąża za zmianami wprowadzonymi gdzie indziej:
uwierzytelniony tak samo, z organizacją w ?organization_id=, wysyła jedną ramkę
JSON na każdy udany zapis przez publiczne API w tej organizacji — resource,
id, action (created, updated albo deleted), surface (console,
api_key, mcp albo assistant) i kto go wykonał — i tylko o wierszach, które
wywołujący może odczytać.
Rozmowy głosowe¶
Rozmowa głosowa ma własny socket, bo rozmowa nie jest pętlą tur:
/api/v1/ws/voice dla członka i
/api/v1/embed/{public_key}/voice/ws dla odwiedzającego w widgecie albo na
hostowanej stronie (wpuszczanie). Oba mówią tymi
samymi ramkami, zdefiniowanymi w backend/app/schemas/voice.py.
Socket konsoli uwierzytelnia się tak jak /ws/agent: token jako wpis
Sec-WebSocket-Protocol access_token.<JWT> (albo ciasteczko z tego samego
originu), nigdy w query stringu, a organizację jako ?organization_id=.
Zamyka się z kodem 4001 bez ważnej sesji i z 4003, gdy wywołujący nie jest
członkiem albo uwierzytelnił się kluczem API organizacji: rozmowa to człowiek
przy mikrofonie, tak jak tura czatu to człowiek przy klawiaturze.
Socket embeda zamyka się z 4029 po przekroczeniu rate limitu
i z 4003 przy każdej odmowie dotyczącej embeda. Każdy heartbeat sprawdza
sesję ponownie, więc rozmowa nie przetrwa wylogowania.
Klient wysyła ramki JSON, a przez relay także binarne audio:
| Ramka | Pola | Kiedy |
|---|---|---|
start |
agent_id, conversation_id, sdp_offer (WebRTC), resume_of i resume_token (ponowne połączenie), context (zmienne embeda) |
Najpierw, w ciągu 10 sekund |
approval |
id, decision (approve albo deny) |
Rozstrzygnięcie zatwierdzenia, na które czeka rozmowa |
mute |
muted |
Relay: mikrofon wyciszony |
played |
bytes |
Relay: ile audio agenta zostało odtworzone |
end |
- | Rozłączenie się |
| binarna | 16-bitowy PCM z częstotliwością wejściową modelu | Relay: audio dzwoniącego |
Serwer wysyła:
| Ramka | Pola | Znaczy |
|---|---|---|
answer |
sdp |
WebRTC: odpowiedź providera dla połączenia peer przeglądarki |
ready |
call_id, run_id, conversation_id, transport, input_sample_rate, output_sample_rate, cap_seconds, resume_token |
Rozmowa jest zestawiona. resume_token jest wysyłany tylko tutaj i tylko temu dzwoniącemu; ponowne połączenie odsyła go z powrotem |
state |
state: connecting, listening, user_speaking, thinking, agent_speaking, interrupted, tool_running, awaiting_approval |
Kto mówi albo co robi agent |
caption |
role, turn_id, part oraz jedno z: delta (dopisz), text (zastąp dotychczasowym sprawdzonym tekstem) albo final |
Napis na żywo |
turn_saved |
message |
Zakończona tura, jako wiadomość, w której została zapisana; zastępuje napisy niosące jej turn_id |
tool |
name, status (running albo done) |
Narzędzie, które agent uruchamia |
approval_required |
id, tool, args, said, timeout_seconds |
Zatwierdzenie dla osoby w rozmowie: dokładne argumenty i to, co według transkrypcji powiedziała |
approval_unavailable |
tool, args |
Wywołanie narzędzia, którego ten dzwoniący nie może zatwierdzić głosem |
approval_settled |
id, outcome (approved, denied albo expired) |
Jak zakończyło się zatwierdzenie |
notice |
kind, message (opcjonalne) |
Coś do powiedzenia słowami: trafienie guardraila, osiągany limit |
limit |
seconds_left |
Czas rozmowy prawie się skończył |
clear_playback |
- | Relay: odrzuć zakolejkowane audio agenta |
ended |
reason, message |
Rozmowa się skończyła |
| binarna | 16-bitowy PCM z częstotliwością wyjściową modelu | Relay: audio agenta |
Odwiedzający widget lub hostowaną stronę dowiaduje się mniej. Jego ready
nie zawiera run_id; cap_seconds to limit rozmowy - najdłuższa rozmowa
agenta albo to, co zostało ze wznawianej rozmowy. Ramki narzędzi podlegają
przełącznikom embeda show_tool_steps i show_tool_results, tak jak
odpowiedź pisana. Bez kroków narzędzi nie jest wysyłana żadna ramka tool, a
tool_running przychodzi jako thinking. Bez wyników narzędzi
approval_unavailable przychodzi z pustym args. Niezależnie od przełączników
turn_saved odwiedzającego zawiera tylko id, role, content,
interrupted i turn_id wiadomości: nigdy identyfikatorów runa, agenta ani
wersji, kosztu ani wywołań narzędzi.
Ponowne połączenie musi udowodnić, że rozmowa należy do niego. start z
resume_of musi zawierać także resume_token z ramki ready tamtego
połączenia. Bez niego albo z błędnym ponowne połączenie jest odrzucane jako
resume_refused, a wskazana rozmowa pozostaje nietknięta. Serwer przechowuje
tylko skrót tokena. Rozmowa złożona już z 20 połączeń nie jest wznawiana
ponownie: jej kolejne ponowne połączenie też jest odrzucane jako
resume_refused.
Socket, który w ciągu 10 sekund nie wyśle start, dostaje ended z no_start.
Rozmowa, która się nie rozpocznie, odpowiada ended ze stałym reason:
no_agent, forbidden, not_configured, bad_offer, data_channel,
resume_refused, call_limit, not_priced, budget, busy albo
line_busy, z message, który można bezpiecznie pokazać. busy pada dopiero
wtedy, gdy rozmowa, którą ta sama osoba rozłączyła przed chwilą, miała do
czterech sekund na zakończenie. Start, który nie powiedzie się z jakiegokolwiek innego
powodu, odpowiada ended z error.
Odwiedzający odrzucony jako
not_configured, not_priced albo budget, oraz rozmowa odwiedzającego
zakończona jako budget, dostaje zamiast
tego unavailable z komunikatem "Calls are not available right now.": kod i
pełny komunikat są do wiadomości operatora agenta. Rozmowa w zarchiwizowanej
konwersacji jest odrzucana jako forbidden, tak jak wiadomość pisana do niej.
Rozmowa, która trwała, kończy się z user, time_limit, budget,
disconnected, revoked, guardrail, shutdown albo error; z time_limit,
gdy osiągnęła najdłuższą rozmowę agenta. Rozmowa odwiedzającego kończy się z revoked, gdy jej
embed zostanie wstrzymany, będzie miał wyłączone rozmowy albo zostanie
skierowany na innego agenta. Żadna ramka nigdy nie niesie poświadczeń
providera.
Idzie z tym pięć route'ów HTTP:
| Route | Bramka | Odpowiada |
|---|---|---|
GET /api/v1/agents/{agent_id}/voice |
agents:run na tym agencie, łącznie z grantem; w przeciwnym razie 404 |
Czy rozmowa by się rozpoczęła, dlaczego nie (no_voice, not_offered albo no_key), transport i najdłuższa rozmowa |
POST /api/v1/agents/{agent_id}/voice/preview |
agents:edit na tym agencie, łącznie z grantem; w przeciwnym razie 404 |
Głos z wersji roboczej sekcji Voice mówiący jedno stałe zdanie, jako audio/wav. W cache per głos i język; RATE_LIMIT_VOICE_PREVIEW_PER_MINUTE na osobę |
GET /api/v1/providers/realtime-models |
agents:view |
Katalog realtime, z którego wybiera sekcja głosu |
GET /api/v1/voice/usage?start=…&end=… |
runs:view |
Sekundy rozmów i liczba rozmów dla rozmów rozpoczętych w chwili start lub później i przed end, według agenta (rozmowy usuniętego agenta jako jeden wiersz bez id i nazwy). end, który nie jest późniejszy niż start, jest odrzucany z 422 |
GET /api/v1/runs?modality=voice |
runs:view |
Tylko rozmowy (text tylko dla wpisanych runów); rozmowa, która połączyła się ponownie, to jeden wiersz z voice_chain, ze wszystkimi połączeniami zsumowanymi |
Zatwierdzenie należące do trwającej rozmowy jest rozstrzygane w rozmowie:
POST /api/v1/approvals/{id} odrzuca je z 400.
Błędy¶
Jedna koperta, wszędzie:
{
"error": {
"code": "NOT_FOUND",
"message": "Agent not found",
"details": { "agent_id": "..." }
}
}
details niesie wartości, a nie wiersze, więc nazywa pole wyjaśniające odmowę i
nigdy nie rekord bazy danych. Gdy odmowa dotyczy czegoś, co wywołujący przesłał,
details.fields jest listą {field, message} — a to właśnie pozwala formularzowi
oznaczyć pole zamiast pokazywać zdanie, którego ktoś musi szukać, przeglądając
stronę ponownie.
Odpowiedź 401 niesie WWW-Authenticate: Bearer. Odczyt spoza najemcy
odpowiada 404, a nie 403, z podanego wyżej powodu.
Run, który się zaczął i nie powiódł, podaje swoje id. POST /agents/{id}/run
odpowiada 502 z MODEL_CALL_FAILED, gdy dostawca modelu odrzucił żądanie albo
go nie dokończył - także każdy model łańcucha fallbacku - oraz 500 z
RUN_FAILED przy każdej innej awarii po starcie runu. Obie odpowiedzi niosą
details.run_id i details.status, a details.conversation_id, gdy run pisał do
rozmowy, więc ponowna próba kontynuuje ten wątek zamiast otwierać nowy; a message mówi
dlaczego słowami platformy, a nie surową odpowiedzią dostawcy. Run jest zapisany
jako nieudany razem z tym, co zużył, więc GET /api/v1/runs/{run_id} go odczyta;
ponowne wysłanie żądania zaczyna nowy run.
Ponawianie, idempotencja i rewizje¶
v1 nie ma warunków na rewizję: żadnego ETag, żadnego If-Match. Zapis jest
stosowany, kiedy przychodzi, i wygrywa ostatni. Idempotency-Key jest
honorowany na jednej trasie - asynchronicznym runie.
GET,PUTiDELETEmożna bezpiecznie powtórzyć. Dwa odczyty czytają dwa razy; usunięcie czegoś, czego już nie ma, dostaje404, które ponowienie może uznać za wykonane.PATCHmożna powtórzyć z tym samym ciałem, i tylko wtedy: dwóch klientów zmieniających jedno pole nadpisuje się nawzajem, zamiast dostać konflikt.POSTnie. PowtórzonyPOST /agents/{id}/runstartuje drugi run i jest rozliczany jak każdy; powtórzone utworzenie robi drugi wiersz. Po timeoucie albo zerwanym połączeniu sprawdź, zanim ponowisz:GET /api/v1/runs?agent_id=…&surface=apilistuje runy od najnowszych, a odczyt listy znajdzie wiersz, który pierwsza próba jednak utworzyła.- Asynchroniczny run tak, z
Idempotency-Key. WyślijPrefer: respond-asynci wybrany przez siebie klucz, najwyżej 255 znaków, a ponowienie z tym samym kluczem i tym samym ciałem odpowie tym samym żądaniem runu, w jakimkolwiek jest stanie. Ten sam klucz z innym ciałem to409, nigdy drugi run. Klucze są per organizacja. Wywołanie synchroniczne z tym nagłówkiem jest odrzucane z400, zamiast pozwalać wierzyć, że ponowienie jest bezpieczne. 429to jedyna odmowa do ponowienia, poRetry-After, który niesie. Każde inne4xxjest ostateczne dla tego żądania.
Konwencje¶
| Prefiks | /api/v1 |
| Tworzenie | POST, 201 |
| Częściowa aktualizacja | PATCH |
| Usuwanie | DELETE, 204, bez ciała |
| Stronicowanie | parametry zapytania skip (≥ 0) i limit (1–100); odpowiedzi listowe niosą items i total |
| Ścieżki | kebab-case |
Stabilność, szczerze¶
Publiczne API ma spisaną obietnicę zgodności; reszta /api/v1 jej nie ma.
Trasy, które może wywołać klucz API, są w osobnym dokumencie OpenAPI pod
/api/v1/public/openapi.json, serwowanym w każdym środowisku. W ramach v1 zmiana
którejś z nich jest addytywna — nowa trasa, nowe opcjonalne pole, nowe pole
odpowiedzi albo wartość enuma — a klient musi ignorować pola odpowiedzi, których
nie zna. Usunięcie albo zmiana nazwy następuje dopiero po co najmniej 90 dniach
oznaczenia jako deprecated w tym dokumencie i wpisie w
notatkach do wydań; zmiana, której nie da się tak przeprowadzić,
trafia do /api/v2, obok v1.
Konsola pokazuje ten sam dokument jako stronę: Ustawienia → API → Dokumentacja API wymienia każdy endpoint z parametrami, body, odpowiedziami i gotowym wywołaniem curl.
Pierwszą zmianą nazwy według tej zasady są aplikacje (#2071): to, co było
artefaktami, jest serwowane pod /api/v1/apps i /api/v1/public/apps, a stare
ścieżki /api/v1/artifacts i /api/v1/public/artifacts odpowiadają tak samo,
oznaczone jako deprecated, aż minie 90 dni. Nazwy uprawnień (artifacts:view,
artifacts:edit), nazwy narzędzi i adresy, z których serwowana jest opublikowana
strona, zachowują swoje nazwy.
Własne trasy konsoli nie mają takiej obietnicy i zmieniają się razem z konsolą; klucz nie może ich wywołać. Biblioteki klienckiej jeszcze nie ma.
Czego publiczne API jeszcze nie robi, żeby nikt nie budował na tym klienta:
- Virtual Tables w nim nie ma; czytanie i zapis tabeli kluczem to #1795.
- Brak streamingu dla klucza. Socket czatu przyjmuje tylko sesję osoby.
- Brak kluczy idempotencji i rewizji, jak wyżej.
Spec agenta ma własną obietnicę: jest wersjonowany i idzie tylko do przodu.
Podsumowanie¶
/docsna deploymencie to generowana referencja; na produkcji jest wyłączona z założenia.- Trzy drogi wejścia: klucz API organizacji, JWT albo ciasteczko konsoli.
- Klucz niesie uprawnienia wydającego zawężone do swoich uprawnień i do jego obecnej roli, jest pokazywany raz i działa wyłącznie w publicznym API.
- Klucz działa we własnej organizacji; sesja czyta
X-Organization-Idi bez niego wraca do organizacji domyślnej, osobistej albo jedynej. Zły nagłówek wygląda jak brakujący zasób. - Uruchomienie agenta przez HTTP to ten sam runner — budżet, zatwierdzenie i audyt obowiązują tak samo.
- Skrypt zdobywa własny klucz bez przeglądarki — logowanie, wydanie,
unieważnienie — a
examples/public-api.shprzechodzi całą tę drogę. - Warto ponawiać tylko
429;POSTnie jest idempotentny, więc sprawdź, zanim go powtórzysz - poza asynchronicznym runem wysłanym zIdempotency-Key. Prefer: respond-asynczamienia run w202i żądanie runu do odpytywania, więc długi run nie trzyma otwartego połączenia.- Publiczne API jest w
/api/v1/public/openapi.jsoni w ramach v1 zmienia się tylko addytywnie, z 90 dniami deprecjacji; SDK jeszcze nie ma.