API — native integration
Everything the web interface does is an ordinary HTTP + SSE API. Anything you can do by clicking, you can do from your own code: run an agent, stream its answer, query a knowledge base, build agents, schedule work, read results.
| Base URL | https://ai.syctra.com/api/v1 |
| Transport | HTTPS only (HTTP is redirected). TLS 1.2+ |
| Encoding | JSON, UTF-8. Content-Type: application/json on every write |
| Streaming | Server-Sent Events (text/event-stream), resumable |
| Auth | Authorization: Bearer <access_token> |
| Versioning | The /v1 prefix is stable. New fields may be added; existing ones are not removed without a new prefix |
1. Authentication
Two credentials exist. Use the right one:
| API key | Email + password | |
|---|---|---|
| For | Servers, scripts, integrations, CI | Interactive apps where a human signs in |
| Obtain | Settings → 🔑 API keys → New key | POST /auth/register + /auth/login |
| Looks like | sk_live_… | JWT pair (access + refresh) |
| Lifetime | Until you revoke it | Access 15 min · refresh 30 days |
Never ship an API key to a browser, a mobile app, or a public repository. It carries the full rights of the account that created it. Anything running on a client device must talk to your own backend, which holds the key.
1.1 API key → access token
An API key is not a bearer token: you exchange it for a short-lived access token, then call any endpoint with that token.
export SYCTRA_API_KEY="sk_live_…"
TOKEN=$(curl -sS -X POST https://ai.syctra.com/api/v1/auth/token \
-H "X-API-Key: $SYCTRA_API_KEY" | jq -r .access_token)
POST /auth/token is the only endpoint that takes the key itself. It accepts it three ways —
X-API-Key: sk_live_…, Authorization: Bearer sk_live_…, or {"api_key": "sk_live_…"} in the
body — and answers:
{ "access_token": "eyJhbGciOiJSUzI1NiIs…", "token_type": "Bearer", "expires_in": 900 }
Cache the token for its expires_in (900 s) and exchange again when it expires. Do not
exchange on every request: the endpoint is throttled to 60 exchanges per minute per IP.
1.2 Managing keys
| Method | Path | Purpose |
|---|---|---|
POST | /api-keys | Create a key. {"name": "production-server"}. The plaintext is in this response only |
GET | /api-keys | List live keys — id, name, prefix, created_at, last_used_at. Never the secret |
DELETE | /api-keys/{key_id} | Revoke. Blocks new exchanges immediately |
Up to 20 live keys per user. Only sha256(key) is stored, so a lost key is unrecoverable —
revoke it and mint another. Revoking stops new exchanges at once; an access token already
issued from that key stays valid until it expires (≤ 15 min), exactly like signing out of a
browser session.
1.3 Email + password (interactive apps)
curl -X POST https://ai.syctra.com/api/v1/auth/register \
-H 'content-type: application/json' \
-d '{"email":"dev@acme.com","password":"…"}' # → a 6-digit code by e-mail
curl -X POST https://ai.syctra.com/api/v1/auth/verify \
-H 'content-type: application/json' \
-d '{"email":"dev@acme.com","code":"123456"}'
curl -X POST https://ai.syctra.com/api/v1/auth/login \
-H 'content-type: application/json' \
-d '{"email":"dev@acme.com","password":"…"}' # → { access_token, refresh_token }
Refresh with POST /auth/refresh {"refresh_token": "…"}. Google and OIDC/SAML sign-in are
available at /auth/google/start and /auth/oidc/start. Login is throttled per account after
repeated failures; sign-ups are capped per IP.
2. Quickstart — ask an agent, stream the answer
The pattern is always the same: start a run, then stream its events. A run is durable — if your process dies, reconnect and replay from where you stopped.
# 1 — start
RUN=$(curl -sS -X POST https://ai.syctra.com/api/v1/chat \
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"message":"Summarise the 2024 turnover trend and chart it","app_name":"data_copilot"}')
RUN_ID=$(echo "$RUN" | jq -r .run_id)
# 2 — stream (resumable)
curl -N https://ai.syctra.com/api/v1/runs/$RUN_ID/events \
-H "Authorization: Bearer $TOKEN"
Python
import json, os, requests
BASE = "https://ai.syctra.com/api/v1"
token = requests.post(f"{BASE}/auth/token",
headers={"X-API-Key": os.environ["SYCTRA_API_KEY"]},
timeout=30).json()["access_token"]
H = {"Authorization": f"Bearer {token}"}
run = requests.post(f"{BASE}/chat", headers=H, timeout=30, json={
"message": "Draft a mutual NDA for an Algerian SARL, then export it to DOCX",
"app_name": "legal_drafting_dz",
}).json()
answer, cursor = [], 0
with requests.get(f"{BASE}/runs/{run['run_id']}/events", headers=H, stream=True) as r:
event = None
for line in r.iter_lines(decode_unicode=True):
if not line:
continue
if line.startswith("id:"):
cursor = int(line[3:].strip()) # keep it: this is your resume point
elif line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
payload = json.loads(line[5:].strip())
if event == "token":
answer.append(payload.get("text", ""))
elif event == "artifact":
print("file:", payload.get("artifact_id"), payload.get("filename"))
elif event in ("done", "error"):
break
print("".join(answer))
Node
const BASE = 'https://ai.syctra.com/api/v1';
const { access_token } = await (await fetch(`${BASE}/auth/token`, {
method: 'POST', headers: { 'X-API-Key': process.env.SYCTRA_API_KEY },
})).json();
const { run_id } = await (await fetch(`${BASE}/chat`, {
method: 'POST',
headers: { authorization: `Bearer ${access_token}`, 'content-type': 'application/json' },
body: JSON.stringify({ message: 'List the risks in this contract', app_name: 'contract_review_dz' }),
})).json();
const res = await fetch(`${BASE}/runs/${run_id}/events`, {
headers: { authorization: `Bearer ${access_token}` },
});
for await (const chunk of res.body) process.stdout.write(Buffer.from(chunk).toString());
3. Conventions
Errors. Standard HTTP status codes with a JSON body {"detail": "…"}.
| Code | Meaning | What to do |
|---|---|---|
400 | Malformed request | Fix the payload; the message names the field |
401 | Missing / expired / invalid token | Exchange the key again |
402 | Insufficient credits | Top up; the run was not started and not billed |
403 | Authenticated but not allowed | Wrong org, agent not granted to your team, or suspended account |
404 | Unknown or not yours | IDs are scoped to your organisation |
409 | Conflict (e.g. session busy) | Read the body; queued work returns 202-like status in POST /chat |
429 | Rate limited | Back off exponentially, honour Retry-After when present |
5xx | Server side | Retry with backoff; runs are idempotent when you send idempotency_key |
Idempotency. Send idempotency_key on POST /chat — replaying the same key returns the
original run (resumed: true) instead of starting and billing a second one.
IDs are UUIDs. Timestamps are ISO-8601 UTC. Send client_tz (IANA, e.g.
"Africa/Algiers") on POST /chat so the agent reasons in your local time.
Rate limits.
| Surface | Limit |
|---|---|
POST /chat and runs | 20 req/s sustained, burst 40, per user |
POST /auth/token | 60/min per client IP |
POST /auth/login | throttled per account after repeated failures |
POST /auth/register | capped per IP per hour |
| Request body | 300 MB (base64 uploads included) |
Concurrency. Runs are serialised per session: send a second message while the first is
still running and it is accepted, queued server-side and executed after it (status: "queued",
with a queue_seq). Use different session_ids to run work in parallel.
4. Billing
Every AI action debits credits from your organisation's balance — the same balance the web app
shows, whether the work came from a browser, an API call, a scheduled task or an embedded
widget. Each run emits a usage event with its token counts, and GET /balance returns the
current balance. A run that cannot be paid for is refused with 402 before any work starts.
5. Security
What the platform enforces:
- Keys are stored hashed (SHA-256). The plaintext exists once, in the creation response.
- Short-lived tokens. Access tokens live 15 minutes; deactivating an account or bumping its token version invalidates its sessions and blocks further exchanges.
- Credential redaction. Agents cannot reveal platform secrets: API keys, SMTP passwords, connection strings and private keys are stripped from tool results, tool errors and model output by a deterministic filter — not by a prompt rule that can be talked around.
- Prompt-injection containment. Once a run ingests untrusted content (an uploaded file, a scraped page, a search result), outbound actions — sending e-mail, posting, writing to memory, arbitrary HTTP — are blocked until you confirm them explicitly.
- Sandboxed execution. Agent-written code runs in a single-use gVisor container with no secrets and no direct network: outbound traffic passes an egress proxy with an SSRF filter.
- Tenant isolation. Every id is scoped to your organisation and re-checked server-side; identity always comes from the token, never from a request field.
- Auditability. Every tool invocation is recorded with its run, organisation and status.
What is on you:
- Keep keys server-side, in a secret manager or environment variable — never in a browser, a mobile bundle, a git repository or a CI log.
- Use one key per system, named for that system, so you can revoke it in isolation.
- Rotate keys periodically: mint the new one, deploy, then revoke the old one.
last_used_attells you when a key is safe to retire. - Treat any key that has appeared in a log, a screenshot or a ticket as compromised — revoke it.
- If you re-expose the API to your own users, enforce your own authorisation: a platform key does not know about your end-users.
Full endpoint catalogue: API reference.