Docs home/🔌 API & native integration

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 URLhttps://ai.syctra.com/api/v1
TransportHTTPS only (HTTP is redirected). TLS 1.2+
EncodingJSON, UTF-8. Content-Type: application/json on every write
StreamingServer-Sent Events (text/event-stream), resumable
AuthAuthorization: Bearer <access_token>
VersioningThe /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 keyEmail + password
ForServers, scripts, integrations, CIInteractive apps where a human signs in
ObtainSettings → 🔑 API keysNew keyPOST /auth/register + /auth/login
Looks likesk_live_…JWT pair (access + refresh)
LifetimeUntil you revoke itAccess 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

MethodPathPurpose
POST/api-keysCreate a key. {"name": "production-server"}. The plaintext is in this response only
GET/api-keysList 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": "…"}.

CodeMeaningWhat to do
400Malformed requestFix the payload; the message names the field
401Missing / expired / invalid tokenExchange the key again
402Insufficient creditsTop up; the run was not started and not billed
403Authenticated but not allowedWrong org, agent not granted to your team, or suspended account
404Unknown or not yoursIDs are scoped to your organisation
409Conflict (e.g. session busy)Read the body; queued work returns 202-like status in POST /chat
429Rate limitedBack off exponentially, honour Retry-After when present
5xxServer sideRetry 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.

SurfaceLimit
POST /chat and runs20 req/s sustained, burst 40, per user
POST /auth/token60/min per client IP
POST /auth/loginthrottled per account after repeated failures
POST /auth/registercapped per IP per hour
Request body300 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_at tells 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.