API¶
How to call SPOT's HTTP APIs: authentication, errors, submitting a message and reading it back. Each route, its parameters, bodies and answers are in the API documents, generated from the code.
Users, the dashboard, the CLI and plugins call the api-gateway: https://api.<your domain> behind the deployment's Traefik, http://localhost:8001 in development. The gateway serves its own document at /openapi.json, and /docs, only when DEBUG is set.
Authentication¶
Users¶
Users log in with the OAuth2 password grant and send the access token, a JWT, as Authorization: Bearer:
TOKEN=$(curl -s -X POST http://localhost:8001/auth/token \
-d "username=admin&password=$SPOT_ADMIN_PASSWORD" | jq -r .access_token)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8001/api/v1/analyze
POST /auth/refreshtrades therefresh_tokenof the same answer for a new pair;POST /auth/client-credentialslogs in an OAuth client.- Each route needs a role:
viewerreads,analystalso submits messages and reads email content,adminalso configures SPOT (users, workflows, plugins, configuration). A role too low is403. - Email content is for analysts and admins: an email's detail, forensics, view and raw message. For a viewer, the analysis routes leave empty the text analyzers quote from the email (the signals' evidence and location, the analyzers' summaries and details).
Plugins¶
A plugin holds its own token (spot_pt_...), read from /run/secrets/spot_plugin_token, and sends it as Authorization: Bearer on every call to the api-gateway and to the knowledge service. Its scopes come from its kind:
| Kind | Scopes | Routes |
|---|---|---|
mail_retriever | ingest | POST /internal/ingest; GET /internal/jobs/{job_id} and POST /internal/jobs/{job_id}/action (its own jobs only) |
analyzer | knowledge:read, content:read | knowledge reads; GET /internal/emails/{email_id}/raw and /parts/{part_id} |
context_provider | knowledge:write | knowledge writes, its own documents only |
responder | results, content:read | GET /internal/results/{job_id}; the content routes |
A missing or invalid token is 401, a token without the route's scope 403. Admins issue and rotate tokens with POST /api/v1/plugins/{plugin_id}/token (see Plugin tokens).
Core calls the routes a plugin serves (/internal/analyze, /internal/sync, /internal/state, /internal/respond) with the plugin's own token too. The internal key (X-Internal-API-Key) is core to core only: the api-gateway does not accept it, and no plugin receives it.
Correlation¶
Every core service and every plugin reads a request's X-Correlation-ID, keeps it when it is 1 to 128 characters of A-Z a-z 0-9 . _ : -, gives the request a new one otherwise, and answers with it, a 500 included: core's services through their Exception handler, a plugin's own 500s through the SDK's app. The ID follows the work it started:
| Where work starts | Its ID |
|---|---|
| An HTTP request to a core service or a plugin | The X-Correlation-ID sent, when valid; else a new one. The answer carries it, a 500 included. |
| A call from a core service to another, or to a plugin | The current ID, unless the caller set the header |
| A message published on RabbitMQ | The current ID, in the message's X-Correlation-ID header |
| A message consumed | Its header's ID, when valid; else a new one, for that message only |
| A scheduled provider sync | A new one per run |
A plugin's calls to SPOT (SpotClient, KnowledgeClient) | The ID of the call being handled |
So a retriever's ingest, its analysis, the analyzers and responders called for it, and their calls back to SPOT share one ID, and every log line written for them carries it (correlation_id, see Logging).
Errors¶
The gateway answers every error with the same envelope:
error is a stable code, message says what went wrong and details adds to it (the limit hit, the fields refused). No error quotes a message's content or a secret: a request that fails validation (422, validation_error) lists the refused fields in details.errors (type, loc, msg), never the values sent.
| Status | error | When |
|---|---|---|
| 400 | bad_request | The request breaks a rule of the route |
| 401 | unauthorized | No valid JWT or plugin token |
| 403 | forbidden | The role or the token's scope is too low |
| 404 | not_found | Unknown resource |
| 405 | method_not_allowed | The route does not take that method |
| 408 | body_timeout | The body did not arrive within intake.body_timeout_s |
| 409 | conflict | The resource is not in a state that allows it (a job not finished, a plugin needing consent, an email stored before 2.0, which has no raw message or view) |
| 413 | message_too_large | A message larger than 25 MiB, or a JSON body larger than 34 MiB |
| 415 | unsupported_media_type | A submission neither message/rfc822 nor JSON |
| 422 | validation_error, empty_message, parse_limit | An invalid request, an empty message, a message that cannot be parsed within the intake limits |
| 429 | rate_limited | Over a rate limit; Retry-After says when to retry |
| 502 | bad_gateway | A core service behind the gateway failed, or the analysis an ingest waits for failed |
| 503 | service_unavailable | Retry later. The gateway takes or serves as many messages as it can, or no parse worker could take the message: these answers carry Retry-After. Or a service the route needs is down (the broker for a submission, the mail-orchestrator for an ingest): without Retry-After |
| 504 | gateway_timeout | A core service behind the gateway did not answer in time (the orchestrator, for a workflow test or a dry run) |
| 500 | internal_error | A fault of SPOT, logged |
Rate limiting¶
Rate limiting is on by default (enable_rate_limiting, in the platform section of spot.yaml or the environment). Over a limit the gateway answers 429 with Retry-After: the whole seconds left in the window of the limit, at least 1, or the window's length when the gateway cannot tell.
| Requests | Limit | Settings |
|---|---|---|
/auth/* | 10 per 60 seconds per client IP | fixed |
| With a valid plugin token | 6000 per period per plugin | plugin_rate_limit_requests, rate_limit_period |
| Any other | 100 per period per client IP | rate_limit_requests, rate_limit_period (60 seconds) |
The plugin limit is sized for a mail retriever ingesting a busy mail flow: 100 requests a second with the default period. Requests whose token is invalid count against the client IP, and a client IP over its limit is refused before its token is checked, so guessing tokens stays limited.
A change of a limit or of the period applies to the next request, without a restart. A window already running keeps its length: a new period applies from the next window.
Intake¶
SPOT takes raw RFC 5322 messages, at most 25 MiB. The gateway parses each one once (spot_sdk.mime), stores the raw message and its view, both encrypted, then queues the analysis. The parse runs in worker processes bounded in time and memory (intake.* in Configuration); a message refused is not stored.
Users: POST /api/v1/analyze¶
Analysts and admins submit the message itself, as a message/rfc822 body:
curl -X POST http://localhost:8001/api/v1/analyze \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: message/rfc822" \
--data-binary @message.eml
or as JSON, the message base64-encoded in raw, with the workflow to run. Left out, the email's route chooses it, else the default workflow runs (Routing):
jq -n --arg raw "$(base64 -w0 message.eml)" '{raw: $raw, workflow_id: "default-workflow"}' |
curl -X POST http://localhost:8001/api/v1/analyze \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @-
The answer names the job and the stored email:
{
"job_id": "3f0e7c1a-...",
"email_id": "9b2d4e6f-...",
"status": "queued",
"workflow": "default-workflow",
"message": "Email analysis job queued successfully"
}
The job's route names the route that chose its workflow (none for a workflow named, the default, or an analysis from before routing).
Poll GET /api/v1/analyze/{job_id} until status is completed, failed or cancelled. A finished job carries the workflow's verdict (malicious, suspicious, spam, benign or unknown), its score (0 to 100, how likely the email is malicious), decided_by, the signals behind the verdict, strongest first, and the whole result, every analyzer's answer included. A failed job is unknown, decided by failed, with its error. Workflows explains how the verdict is decided.
Analysts and admins cancel a job queued or running with DELETE /api/v1/analyze/{job_id}: a running job's analysis stops at once, and a queued one never starts. The job is recorded cancelled, unknown, decided by cancelled, and no responder is called. The answer is {"job_id": "...", "status": "cancelled"}, also for a job already cancelled; a completed or failed job is 409 and an unknown one 404. A 503 asks to retry the cancel. When the database or the analyzer-orchestrator is not available, nothing was cancelled. When the orchestrator could not record the cancel, it keeps the cancel pending and does not run the job meanwhile, unless it restarts. A retriever's ingest has no job record until the analyzer-orchestrator picks it up, so it can be cancelled only from then on.
GET /api/v1/analyze/stats counts the analyses in total, by_status and by_verdict, and GET /api/v1/analyze/stats/daily?days=N (1 to 90, 30 by default) gives the same for each of the last N UTC days, with failed, cancelled, and the median and 95th percentile of the completed analyses' execution times. A cancelled job counts only as cancelled: in by_status and in its day's cancelled, never in total or by_verdict, where a failed job counts as unknown.
To analyse a stored email again, analysts and admins call POST /api/v1/emails/{email_id}/analyze, without a body or with JSON naming the workflow (left out, the stored email's route chooses it, else the default workflow runs):
curl -X POST http://localhost:8001/api/v1/emails/$EMAIL_ID/analyze \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"workflow_id": "default-workflow"}'
Nothing is stored again: the job runs on the stored email, is listed with its analyses (GET /api/v1/analyze?email_id=...), and the answer is the same as a submission's. An unknown email is 404, and an email stored before 2.0, which has no raw message or view, 409.
The workflow dry run, POST /api/v1/workflows/{workflow_id}/test (analysts and admins), takes the same bodies, and is never routed. It stores the message for the length of the run, as an email no list shows, so analyzers fetch its raw copy and its parts, and deletes it when the run ends; it stores no job and no analysis.
Mail retrievers: POST /internal/ingest¶
A retriever sends an IngestionRequest: the message base64-encoded in raw, and optionally the SMTP envelope, a priority, and wait_for_verdict with its timeout_ms. It names no workflow: the email's route chooses it, else the default workflow runs, and a route's timeout_ms caps the wait. A request with a workflow_id is refused (422). The body may be up to 34 MiB. The answer is an IngestionResult: accepted at once, or, with wait_for_verdict, analyzed with the verdict and the score, or timeout. The SDK's SpotClient.ingest sends it.
Routing¶
The routing document is three sections of spot.yaml, {recipient_groups, routing, shadow}, as the file holds them (Routing):
GET /api/v1/routing(analysts and admins: the groups list staff addresses) answers it.PUT /api/v1/routing(admins) replaces it whole and answers it as saved; an empty document clears the routing. A document that breaks a rule is refused with 422 (validation_error), nothing saved: one the model refuses (a route named twice, an unknown group, a malformed entry or domain) lists its errors indetails.errors, by field and rule, never the value; one naming a workflow that is not loaded, orsender_domainswithoutmail.trusted_authserv_ids, has the message naming the route. A saved document is a commit ofspot.yaml's history by the user.GET /api/v1/workflows/{workflow_id}/shadow?limit=50&offset=0(viewers) lists the workflow's shadow differences, newest first: the job, the email, production's workflow, verdict and score, and the shadow workflow's version, verdict, score anddecided_by; never a result. The page also gives the rate of the differences over the lastdaysUTC days, today's included (?days=30, 1 to 90):compared, the production results the workflow ran on (an email analysed again counts again), anddiffered, those on which its verdict class differed (how they are counted).
Observe and enforce¶
Each job is in observe or enforce: enforce only when its mail connection and its route both are; a user's submission is always in observe (Observe and enforce). The job's status gives its mode and its actions: what was done with the email, or would have been in observe.
PUT /api/v1/config/plugin/mail_retriever/{plugin_id}/mode(admins), with{"mode": "enforce"}or{"mode": "observe"}, switches a mail connection. Nothing restarts: the jobs queued from then on take the new mode.GET /api/v1/analyze/actions?days=7(viewers, 1 to 90 days) counts the actions of the last days by actor, kind, action, mode and whether each was applied, and gives every mail connection's mode inconnections.POST /internal/jobs/{job_id}/action(scopeingest) is how a retriever reports what it did with one of its jobs' messages, anActionReportofactionandapplied:204,404for a job that is not its own,422forappliedon a job in observe. A second report replaces the first.
Content¶
The gateway keeps each message as received and its parsed view, both encrypted, for data_retention_days.
- Users (analysts and admins):
GET /api/v1/emails/{email_id}/viewreturns the view, as analyzers receive it (spot_sdk.EmailView), andGET /api/v1/emails/{email_id}/rawthe message as received (message/rfc822). - Plugins with
content:read:GET /internal/emails/{email_id}/rawreturns the message, andGET /internal/emails/{email_id}/parts/{part_id}one decoded part, with its media type. Parts are numbered as in the view (Part.part_id); a container part (multipart/*,message/*) is not served. - Responders with
results:GET /internal/results/{job_id}returns the workflow'sOrchestrationResult;409while the job runs,404for a job unknown, failed or analysed before 2.0.
Each read of a raw message or a part is logged, without the content. The gateway serves at most intake.max_content_in_flight raw messages, parts and views at once, and answers 503 past it. An email stored before 2.0 has no raw copy or view: those routes answer 409, and 404 only for an unknown email.
Queues¶
The dashboard's Health and Queues pages read GET /api/v1/observability/services and GET /api/v1/observability/queues (viewers). An admin replays the messages a queue parked in its dead-letter queue with POST /api/v1/observability/queues/{queue}/replay, for orchestrator.analysis, orchestrator.dispatch, orchestrator.shadow and orchestrator.benchmark (what a replay of the last does: Messages in a dead-letter queue). It answers {"queue": ..., "replayed": N}; ?limit=N (at least 1) moves at most N. Only the messages parked when the replay starts move, each at least once. A queue that parks nothing is 404, and a broker failure 503: what was not moved stays parked, and a second replay moves the rest. Fix the cause first, then replay one (?limit=1) and check that it is handled before replaying the rest (Configuration).
spot.yaml's history¶
Every change of spot.yaml is a commit, by the user who made it (Configuration). Admins read and roll it back:
GET /api/v1/config/history?limit=50&offset=0lists the commits, newest first: each one'sid,short_id,author(a username, orSPOT),at,message, and thesectionsandworkflowsit changed, withtotal.GET /api/v1/config/history/{commit_id}gives a commit, itsdifffrom the commit before it (from no file for the first), androllback_diff: what a rollback to it would change in the settings in force, as SPOT writes them (a hand edit not yet reloaded is not shown),nullwhen itsspot.yamlno longer validates. Each sealed secret reads(sealed), or(sealed, changed)where its value changed.POST /api/v1/config/history/{commit_id}/rollbackrestores every section of that commit butplugins:, as a commit of its own,Roll back to <12 digits>. It answers thatcommitand thesectionsit changed from the settings in force before it.commitisnullwhen none was recorded: when nothing changed (no sections either), while the history is paused (a start or a reload is sealing secrets, and its commit records the rollback), or when the commit failed (logged; the next commit records the change). A commit whosespot.yamlno longer validates is422, naming each field and the rule it breaks, and nothing is written.
commit_id is a commit's full id or a prefix of at least 7 hex digits that only it starts with: anything else is 422, and an id no commit (or more than one) has is 404.
A workflow's versions¶
The gateway numbers a workflow's versions: every save stores 1 + the highest version its id has had, in force or in its history, so a version number names one version, even after a config rollback or a hand edit lowered the one in force. A hand edit that changes a workflow should raise its version past that highest (spot-cli workflow history <id>): one that keeps the number gives it a second definition. POST and PUT /api/v1/workflows drop the body's version and created_by, whatever they hold: the author is the user who created the workflow, and a restore keeps the restored version's. A workflow's versions are read from spot.yaml's history:
GET /api/v1/workflows/{workflow_id}/history?limit=20&offset=0(viewers) lists the commits that changed it, newest first: each one'scommit, the workflow'sversionin it, anddeletedfor the one that deleted it.GET /api/v1/workflows/{workflow_id}/history/{commit_id}(viewers) gives theworkflowas that commit stores it (nullwhen it holds none) and itsdifffrom the commit before.POST /api/v1/workflows/{workflow_id}/history/{commit_id}/restore(admins) saves that version as the next one, a deleted workflow included. A workflow of contract 1 is converted first; one that breaks a rule is400. An unknown commit, or one without the workflow, is404.POST /api/v1/workflows/{workflow_id}/clone(admins), with{"id": "new-id", "name": "New name"}, copies a workflow under a new id (letters, digits,-and_, at most 100) and answers as a create.nameis the source's when not given. An unknown source is404, an id already stored409.
While spot.yaml's history cannot be read (an object or a ref of history.git missing or damaged), these routes and the saves that number a version (POST and PUT /api/v1/workflows, a restore, a clone) answer 503, naming it, and save nothing.
Benchmark runs¶
spot-cli benchmark run records the run it makes, and the platform runs backtests of its own (Stored runs). A run holds what ran and each email's verdict, score and times, never an email's content, so viewers read it; analysts and admins start, record, cancel and delete.
POST /api/v1/benchmarks(analysts) starts a run, with theworkflow_idand theemailsit takes (the datasetfileswith their SHA-256, thetotal, thelabelfilter,shuffle,seedandmax_emails; at most 50 000 emails). The gateway takes the snapshot now: the workflow's version,spot.yaml's last commit, SPOT's version, and each analyzer's version and image. It answers201with the run, and the audit log holdsbenchmark.start. A workflow that does not load is404.emailsis one of three.{"files": ...}as above is spot-cli's run; it starts without the message broker.{"dataset": "<id>"}(a dataset of promoted copies; an unknown one is404) and{"mail": {"since": ..., "until": ..., "sources": [...], "labelled_only": false, "limit": 500}}(the stored mail received in the period, the most recent first;untildefaults to now, which the run records, nosourcesis every source,limitis 1 to 10 000 and defaults to 500) are the platform's: the gateway queues one message per email, onorchestrator.benchmark, and the analyzer-orchestrator runs them one at a time, recording the results. A platform run needs the message broker (503without it), takes 10 000 emails at most (422for a larger dataset, and for a selection that matches no email), and fingerprints the emails inselection_sha256. One platform run runs at a time: a start while one runs is409naming it, a rule the database keeps even for two simultaneous starts.POST /api/v1/benchmarks/{run_id}/results(analysts) records up to 1000 results, each in place of the one of the samekey, so a batch sent again leaves one result per email. It answersrecordedoftotal. A run that has ended is409, and an unknown run404. The platform records a platform run's results itself: for it, this and the next call are409.POST /api/v1/benchmarks/{run_id}/finish(analysts) ends the run and counts its metrics over the results recorded: overall, per dataset and per analyzer, with the median and 95th percentile of the times. A run already ended is answered as it is. A platform run's metrics, counted when it ends, also hold production's (baseline) when its results carry production's verdicts: counted from them over the same labelled emails, an email without one as failed.POST /api/v1/benchmarks/{run_id}/cancel(analysts) cancels a running run, asbenchmark.cancelin the audit log, and counts its metrics over the results recorded so far. The messages of a platform run still queued are skipped; spot-cli's next batch is refused and it stops, unless the run is resumed (Stored runs). A cancel is final: a run that has ended, whoever ended it, answers409.GET /api/v1/benchmarks?workflow_id=...&limit=50&offset=0(viewers) lists the runs, newest first, withtotal;limitis 1 to 100.GET /api/v1/benchmarks/{run_id}(viewers) gives a run with its snapshot, the emails recorded so far and, once ended, its metrics.GET /api/v1/benchmarks/{run_id}/results?missed=false&differs=false&limit=50&offset=0(viewers) lists its results in the order recorded;missed=truekeeps those the workflow got wrong, anddiffers=truethose whose verdict differs from the one production gave. A result of a stored email holdsbaseline_verdictandbaseline_job_id, the production analysis that verdict is of: the email's latest completed analysis, whichever workflow ran it.DELETE /api/v1/benchmarks/{run_id}(analysts) deletes the run and its results, asbenchmark.deletein the audit log.
Every request is under the rate limit.
Labels, datasets and sender lists¶
An analyst corrects a verdict by labelling the email, listing its sender and promoting it into a dataset (Correcting a verdict). Analysts and admins use all of these routes, except that viewers read the datasets: a label carries a note and names a sender.
PUT /api/v1/emails/{email_id}/labelsets the label, in place of the previous one: averdict(malicious,suspicious,spamorbenign, neverunknown) and anoteof at most 2000 characters.GETreads it, with its note;DELETEremoves it. An unknown email or a missing label is404. A dataset's copy keeps its label:DELETEanswers409, andPUTrelabels it.GET /api/v1/labels?verdict=...&since=...&limit=50&offset=0lists the labels, newest first, withtotal. Each names its email's sender and, for a dataset's copy, itsdataset_id(nullfor a received email), so a copy's label is told from its original's.sinceis an ISO 8601 time with its offset (one without is422);limitis 1 to 100.POST /api/v1/datasets(id,name,description) creates a dataset; an id in use is409.GET /api/v1/datasetslists them with their counts andGET /api/v1/datasets/{dataset_id}gives one with a page of its copies, each with its subject and sender (viewers too).POST /api/v1/datasets/{dataset_id}/emails(email_id) copies a labelled email into the dataset and answers201with the copy. It is409when the email has no label or no raw message, when the dataset holds its message already or has its 10 000 emails, or when its stored view is unreadable.DELETE /api/v1/datasets/{dataset_id}/emails/{email_id}removes a copy, by the copy's id, which deletes it;DELETE /api/v1/datasets/{dataset_id}deletes the dataset and its copies and answers how manyemails. The emails they were made from stay.GET /api/v1/sender-listsgives theblockandallowlists.POST /api/v1/sender-lists/{which}(alloworblock;domain, and optionally theemail_idof the label it comes from) adds a domain and answers201with the lists;DELETE /api/v1/sender-lists/{which}/{domain}removes one. A domain already on a list is409, and the allow list is422whilemail.trusted_authserv_idsis empty. Each change is a commit of spot.yaml's history by the user. What a list does is a workflow's policy to decide (Sender lists).
Plugin resources¶
An admin overrides an installed plugin's memory and CPU limits with PUT /api/v1/plugins/{plugin_id}/resources and a body of memory_mb (at least 128), cpus (above 0, at most 64) or both, such as {"memory_mb": 3072}; a value left out keeps the limit in force. DELETE on the same path removes the override: the image's request applies again, else 1024 MB and 1 CPU. Both restart an enabled plugin and answer the installed plugin with resources (the override), requested_resources (the image's request), effective_resources (the limits in force) and restarting. A plugin that is not installed, a manual entry included, is 404, and a body that gives neither value, or another key, 422. A plugin that cannot be restarted with the new limits gets its previous ones back, saved again and restarted with them: when Docker refused the new limits, such as more CPUs than the host has, the answer is 422 with Docker's reason, and when the restart failed otherwise, 503. A 503 also says that spot.yaml could not be saved (nothing changed). See Plugin resources.
Audit log¶
The api-gateway records who did what: sign-ins, plugin lifecycle actions, user accounts, plugin sources, labels, datasets, reads of a raw message, deletions of stored data and replays of parked messages (Audit log). Configuration changes are the commits of spot.yaml's history. Admins read the log:
GET /api/v1/audit?limit=50&offset=0lists the entries, newest first, withtotal: how many pass the filters.limitis 1 to 500.- The filters, each optional, all applied:
action: an action (plugin.install), or a family ending in.(plugin.), in lowercase letters and_;actor: who acted, as named then;actor_id: who acted, by an id a rename keeps (a user's UUID, an OAuth client's id);target: what was acted on;outcome:successorfailure;sinceanduntil: ISO 8601 times, both included; one without an offset is UTC.
actor, actor_id and target hold at most 255 characters. A filter that breaks its rule is 422.
An entry holds at (UTC), actor and actor_id, action, target, outcome, details, ip_address, user_agent and correlation_id. details names what changed; a failure adds status, the HTTP status the route answered. A failed sign-in names the account only when it exists: actor and actor_id are null otherwise.
| Action | Route | Target | Details |
|---|---|---|---|
auth.login | POST /auth/token | failure: reason (unknown_account, bad_password, inactive) | |
auth.client_login | POST /auth/client-credentials | failure: reason (refused) | |
plugin.install | POST /api/v1/plugins/install | plugin | version, kind, egress, consented, settings, secrets (names) |
plugin.update | POST /api/v1/plugins/{id}/update | plugin | from_version, to_version, consented, secrets (names), and settings (names) when sent |
plugin.uninstall | DELETE /api/v1/plugins/{id} | plugin | |
plugin.start, plugin.stop, plugin.restart | POST /api/v1/plugins/{id}/start, /stop, /restart | plugin | |
plugin.settings | PUT /api/v1/plugins/{id}/settings | plugin | settings, secrets (names) |
plugin.token | POST /api/v1/plugins/{id}/token | plugin | |
plugin.resources | PUT, DELETE /api/v1/plugins/{id}/resources | plugin | memory_mb, cpus as sent and reset: false for the PUT; reset: true for the DELETE |
plugin.add | POST /api/v1/config/plugin/{kind} | plugin | kind |
plugin.configure | PUT /api/v1/config/plugin/{kind}/{id} | plugin | fields, secrets (names) |
plugin.enable, plugin.disable | POST /api/v1/config/plugin/{kind}/{id}/enable, /disable | plugin | |
plugin.remove | DELETE /api/v1/config/plugin/{kind}/{id} | plugin | |
plugin.mode | PUT /api/v1/config/plugin/mail_retriever/{id}/mode | plugin | mode |
plugin.sync | POST /api/v1/plugins/context_provider/{id}/sync | plugin | a failure when the trigger failed, though the route answers 200 |
user.create | POST /api/v1/users, POST /auth/register | username | role |
user.update | PUT /api/v1/users/{id} | username | fields; username, the new name, on a rename |
user.role | PUT /api/v1/users/{id}/role | username | role |
user.password | PUT /api/v1/users/{id}/password | username | |
user.activate, user.deactivate | POST /api/v1/users/{id}/activate, DELETE /api/v1/users/{id} | username | |
source.add, source.update, source.remove, source.enable, source.disable | /api/v1/plugin-sources... | source id | type (not for source.remove) |
email.delete | DELETE /api/v1/emails/{email_id} | email id | |
email.raw | GET /api/v1/emails/{email_id}/raw | email id | |
email.label | PUT /api/v1/emails/{email_id}/label | email id | verdict, and replaced, the class it replaces (null when none), never the note |
email.unlabel | DELETE /api/v1/emails/{email_id}/label | email id | replaced, the class removed |
dataset.create | POST /api/v1/datasets | dataset id | |
dataset.promote | POST /api/v1/datasets/{dataset_id}/emails | dataset id | email_id, and on success copy_id, verdict |
dataset.remove | DELETE /api/v1/datasets/{dataset_id}/emails/{email_id} | dataset id | copy_id |
dataset.delete | DELETE /api/v1/datasets/{dataset_id} | dataset id | emails: how many copies were deleted |
knowledge.delete | DELETE /api/v1/knowledge/sources/{source}/documents/{doc_id} | <source>/<id> | |
benchmark.start | POST /api/v1/benchmarks | workflow id | run, emails |
benchmark.cancel | POST /api/v1/benchmarks/{run_id}/cancel | run id | |
benchmark.delete | DELETE /api/v1/benchmarks/{run_id} | run id | |
queue.replay | POST /api/v1/observability/queues/{queue}/replay | queue | messages: how many were moved back |
A user's routes name the user as it was before the action, by its id when no user has it.
The documents¶
| Document | Served by | Called by |
|---|---|---|
gateway.openapi.json | api-gateway | users, the dashboard, the CLI, plugins (/internal/*) |
knowledge.openapi.json | knowledge service (SPOT_KNOWLEDGE_URL) | context providers and analyzers; routes tagged "Core only" are core's |
plugin-<kind>.openapi.json: analyzer, context provider, mail retriever, responder | every plugin of that kind, through the SDK's create_plugin_app | core |
analyzer-orchestrator.openapi.json | analyzer-orchestrator | the api-gateway; the mail-orchestrator probes its /health |
mail-orchestrator.openapi.json | mail-orchestrator | the api-gateway only |
plugin-manager.openapi.json | plugin-manager | the api-gateway only |
API documents renders each of them. make docs:openapi regenerates them, and the CI job openapi fails when the committed documents differ from the code.
Related documentation¶
- Configuration -
spot.yaml, plugins, workflows and intake limits - Admin guide - plugin tokens, secrets and resources
- Writing plugins - the SDK's guide to the plugin routes and clients