API REFERENCE

The API

One base URL, one Bearer key, and a handful of endpoints. Everything the widget does, you can do server to server.

Base URL

Every endpoint lives under a single host. Paths are versioned with a /v1 prefix.

text
https://agentifys.ai

Authentication

Authenticate every request with your project key as a Bearer token. Keys start with era_ and belong to one project. Create and rotate them in the admin under Settings.

text
Authorization: Bearer era_your_project_key
The embedded widget sends this key from the browser as a Bearer token, so a widget install publishes the key to anyone who reads your HTML. That is the shape the allowed-origins list exists to contain. If you integrate from your own server instead, keep the key server-side: never put it in a VITE_ / NEXT_PUBLIC_ variable or any bundle you ship.

A missing or wrong key returns PROJECT_INVALID_KEY with a 401. Once the key resolves, most endpoints run the origin check described below.

Allowed origins

A project carries a list of allowed origins, empty by default. While it is empty the check is skipped and any caller holding a valid key is served. The moment the list has one entry, every request to a checked endpoint must present a matching Origin header or it is refused with ORIGIN_NOT_ALLOWED and a 403.

The comparison is an exact string match on scheme + host + port, after any trailing slash is stripped from the incoming header. There are no wildcards and no subdomain matching. https://example.com does not admit https://www.example.com, and https://app.example.com does not admit https://app.example.com:8443. Add every origin you actually serve from.

text
https://app.example.com
https://www.example.com
http://localhost:5173
The check is not browser-only. A server-side caller sends no Origin header at all, the absent header is read as the empty string, and the empty string never matches a non-empty list. A backend calling a project that has any allowed origin configured is refused exactly like an unlisted website.

The check runs on /v1/chat, /v1/history, /v1/feedback, DELETE /v1/sessions/{id}, and every /v1/knowledge, /v1/scheduled-tasks, and /v1/oauth/mcp route. Two endpoints skip it: the vault (/v1/credentials, all three methods), which is server-to-server by design, and GET /v1/project-config, which the widget loads before anything else.

This is an application-level check, not a CORS restriction. CORS headers are permissive, so the browser will hand you the response rather than blocking it, and what you read is a 403 JSON body.

Two integration shapes

How you integrate decides what the allowlist should contain. Pick one per project.

Browser-direct. The embedded widget, or your own front-end code calling agentifys.ai from the page. Every request carries the page's origin. Fill the allowlist in: it is the only thing standing between your key and anyone who copies it out of your HTML.

Server-proxy. Your backend holds the key and calls Agentifys; the browser only ever talks to your server. Those requests arrive with no origin, so the allowed-origins list must be left empty. One entry is enough to break every call your backend makes. Access control in this shape is your own perimeter — the session or token your server already checks before it proxies.

A single project cannot do both. If you need a widget with a populated allowlist and a server-side integration, use two projects with two keys. Forwarding a hand-written Origin header from your proxy does make the call pass, but the header is then a value your own server chose, so the allowlist stops being a control and is only ceremony.

User identity

Most endpoints take a user_context object that names the end user. This is what powers per-user memory, private knowledge, the vault, and scheduling.

A project runs in one of two identity modes. In open mode the identity is trusted as sent, good for personalization. In signed mode the identity is tamper-proof: you sign it server side with an HMAC secret, and that gates private uploads, the vault, per-user MCP OAuth, and the My-tasks panel.

FieldTypeDescription
idstringStable unique id for the end user in your system. Required in signed mode, where a verified context without one is rejected with IDENTITY_ID_REQUIRED. In open mode the whole object may be empty, and the user is treated as anonymous.
namestringDisplay name.
emailstringEmail address.
rolestringYour app role, usable in policies.
dataobjectAny extra fields your tools or policies read.
_tsnumberUnix seconds, added at sign time. Signed mode only.
_sigstringHMAC-SHA256 hex over the canonical identity. Signed mode only.

Where the identity travels depends on the route, and the pattern is not guessable from the HTTP method. There is no fallback: a route reads exactly one location, and an identity sent anywhere else is treated as absent. This table is the whole matrix.

FieldTypeDescription
POST /v1/chatbodyuser_context object in the JSON body.
POST /v1/feedbackbodyuser_context object in the JSON body.
PUT /v1/credentialsbodyuser_context object in the JSON body, alongside name and value.
POST /v1/scheduled-tasks/seenbodyuser_context object in the JSON body.
PATCH /v1/scheduled-tasks/{task_id}bodyuser_context object in the JSON body, alongside status.
POST /v1/oauth/mcp/startbodyuser_context object in the JSON body, alongside server_id.
POST /v1/knowledgeformuser_context as a multipart form field holding a JSON string, alongside file.
GET /v1/historyheaderX-Era-User.
GET /v1/scheduled-tasksheaderX-Era-User.
GET /v1/oauth/mcp/serversheaderX-Era-User.
DELETE /v1/sessions/{session_id}headerX-Era-User. The one DELETE that reads the header rather than the query param.
GET /v1/knowledgequeryuser_context query param, URL-encoded JSON.
GET /v1/credentialsqueryuser_context query param, URL-encoded JSON.
DELETE /v1/knowledge/{doc_id}queryuser_context query param, URL-encoded JSON.
DELETE /v1/credentials/{name}queryuser_context query param, URL-encoded JSON.
DELETE /v1/scheduled-tasks/{task_id}queryuser_context query param, URL-encoded JSON.
DELETE /v1/oauth/mcp/connection/{server_id}queryuser_context query param, URL-encoded JSON.

The X-Era-User header holds the same JSON, URL-encoded. Encode it even when the identity is pure ASCII: a raw JSON value is accepted, but a name with a non-Latin-1 character in it will be rejected by the browser before the request leaves.

text
X-Era-User: %7B%22id%22%3A%22u_42%22%2C%22_ts%22%3A1735689600%2C%22_sig%22%3A%22...%22%7D
Signed requests are valid for a 300-second window. Sign per request, do not cache a signature. The signing recipe lives in the identity guide.

Content types

Requests with a JSON body send Content-Type: application/json. Knowledge upload is the one exception, it is multipart/form-data.

Most responses are JSON. The chat endpoint is different: it always answers with text/event-stream (Server-Sent Events).

That is not negotiated. POST /v1/chat never reads your Accept header, and there is no way to ask it for a single JSON reply — whatever you send, the response is a stream you have to read to the end. The examples include the header below because it states what you expect, but sending it or omitting it makes no difference to what comes back.

text
Accept: text/event-stream

Endpoints