AI assistant API
Ce contenu n’est pas encore disponible dans votre langue.
These endpoints are what Settings → AI assistant and the AI assistant · advice only panel on an alert use. The assistant advises: asking it never changes the alert. Before you switch it on, read AI assistant security, which says what is sent to the model’s provider and how it is masked.
All of them need a signed-in session; add -b ~/.sp-cookies to every request below.
| Endpoint | What it does | Permission | Success |
|---|---|---|---|
GET /api/v1/ai/settings |
The settings, this month’s usage and the agreement figures | settings.view or settings.manage |
200 |
PUT /api/v1/ai/settings |
Replace the settings | settings.manage |
200, or 202 |
POST /api/v1/ai/allowance-requests |
Ask the service provider for more calls this month | settings.manage |
200 |
GET /api/v1/alerts/{id}/ai-review |
The latest advice on an alert, and whether the assistant can be asked | alerts.view |
200, or 404 |
POST /api/v1/alerts/{id}/ai-review |
Ask the assistant about an open alert | cases.work and entities.view |
200 |
GET /api/v1/cases/{id}/ai-summary |
The latest summary of a case, and whether the assistant can be asked | cases.view |
200, or 404 |
POST /api/v1/cases/{id}/ai-summary |
Ask the assistant to summarise a case | cases.work and entities.view |
200 |
POST /api/v1/ai/batches |
Ask the assistant about many alerts or cases at once | cases.work and entities.view |
200 |
GET /api/v1/ai/batches/{id} |
How a batch is doing | cases.work |
200, or 404 |
GET /api/v1/ai/batches?kind= |
Your latest batch of a kind | cases.work |
200 |
GET /api/v1/ai/available |
Whether the assistant is switched on | alerts.view or cases.view |
200 |
GET /api/v1/reports/{id}/ai-narrative |
The latest draft of a report’s narrative, and whether one can be asked for | reports.draft and entities.view_pii |
200, or 404 |
POST /api/v1/reports/{id}/ai-narrative |
Ask the assistant to draft the narrative of a draft STR or SAR made from a case | reports.draft and entities.view_pii |
200 |
GET /api/v1/rules/{id}/ai-tuning |
The latest tuning suggestions for a rule, and whether the assistant can be asked | rules.view |
200, or 404 |
POST /api/v1/rules/{id}/ai-tuning |
Ask the assistant how a rule is doing and what would be worth testing | rules.author |
200 |
A support session can read the settings and can never read or ask for a
review, a case summary or a narrative draft, nor start or read a batch or call
/api/v1/ai/available. With the configuration scope it can read a rule’s
tuning suggestions, which hold no customer data, and can never ask for them.
Settings
Section titled “Settings”GET /api/v1/ai/settings
Section titled “GET /api/v1/ai/settings”curl -s -b ~/.sp-cookies https://speculapro.yourbank.com.gh/api/v1/ai/settings{ "enabled": true, "key_source": "own", "model": "", "auto_review": false, "provider": "anthropic", "has_own_key": true, "installation_key": false, "gateway": false, "default_model": "claude-sonnet-5-5", "models": [ { "id": "claude-sonnet-5-5", "name": "Claude Sonnet 5.5" }, { "id": "claude-haiku-5-5", "name": "Claude Haiku 5.5" }, { "id": "claude-opus-5-5", "name": "Claude Opus 5.5" } ], "providers": [ { "id": "anthropic", "name": "Anthropic (Claude)", "choose_model": true, "needs_base_url": false }, { "id": "openai", "name": "OpenAI", "choose_model": false, "needs_base_url": false }, { "id": "google", "name": "Google (Gemini)", "choose_model": false, "needs_base_url": false }, { "id": "mistral", "name": "Mistral", "choose_model": false, "needs_base_url": false }, { "id": "custom", "name": "Another OpenAI-compatible API", "choose_model": false, "needs_base_url": true } ], "installation_provider": "anthropic", "monthly_calls": 0, "topup_calls": 0, "can_request_more": false, "calls_on_installation_key": 0, "usage": [ { "feature": "alert_review", "model": "claude-sonnet-5-5", "key_source": "own", "calls": 42, "input_tokens": 61230, "output_tokens": 9870 } ], "reviewed_30d": 30, "agreed_30d": 24, "updated_at": "2026-10-12T09:30:00Z"}| Field | Type | Meaning |
|---|---|---|
enabled |
boolean | The assistant may be asked about this workspace’s alerts. false until someone switches it on |
key_source |
string | Whose API key is used: installation (the operator’s, metered for the workspace) or own (the workspace’s) |
model |
string | The model the workspace chose. Empty uses default_model; with a provider other than anthropic on the workspace’s own key it is that provider’s id for the model and is never empty |
auto_review |
boolean | The worker reviews new open alerts without being asked |
provider |
string | With key_source own: whose API the key is for, anthropic, openai, google, mistral or custom. Always anthropic with key_source installation, where installation_provider applies instead |
base_url |
string | With provider custom: the address of the OpenAI-compatible API. Omitted otherwise |
has_own_key |
boolean | A key of the workspace’s own is stored. The key itself is never returned |
installation_key |
boolean | The installation has a key this workspace can use: SP_AI_API_KEY is set and the plan doesn’t leave it out. When false, only own is accepted. A workspace that is enabled on installation while this is false can’t be answered |
gateway |
boolean | The installation’s provider is Claude and it has its own address for calls (SP_AI_BASE_URL). Calls on the installation’s key and on a workspace’s own Claude key go through it; calls to another provider with the workspace’s own key do not. false when the installation’s provider is not Claude: its address is then that provider’s, and a workspace’s own Claude key goes straight to the Claude API |
default_model |
string | The installation’s default (SP_AI_MODEL) |
models |
array | The Claude models a workspace can choose, each with id and name |
providers |
array | The providers a workspace can bring its own key for: id, name, choose_model (the model is picked from models; otherwise its id is typed) and needs_base_url |
installation_provider |
string | Whose API the installation’s key is for (SP_AI_PROVIDER) |
monthly_calls |
integer | Calls the workspace may make on the installation’s key this calendar month: its plan’s number or the installation’s default (SP_AI_MONTHLY_CALLS), plus topup_calls. 0 is no limit |
topup_calls |
integer | Of those, calls the provider added for this month. They don’t carry over. 0 where no limit applies |
can_request_more |
boolean | POST /api/v1/ai/allowance-requests is available: a limit above 0 applies (the plan includes the installation’s key), the installation runs a provider console and it has a key |
allowance_request |
object | The workspace’s latest request for more calls, whatever its state; omitted if none was ever sent or no limit applies. See Asking for more calls |
calls_on_installation_key |
integer | Calls made on the installation’s key this month |
usage |
array | This calendar month (UTC), one entry per feature, model and key: calls, input_tokens, output_tokens. A call is counted before it is made, so calls includes calls that failed at the provider; tokens are added when an answer comes back |
reviewed_30d |
integer | Alerts the assistant advised on in the last 30 days whose outcome is now known |
agreed_30d |
integer | Of those, how many a person then handled the way it advised |
updated_at |
time | When the settings last changed; omitted if they never have |
How the two agreement figures are counted:
- Only the latest review of each alert counts, and only when it recommended
likely_false_positiveorneeds_investigation. - An alert that went into a case agrees with
needs_investigation. - An alert closed as
false_positiveorno_actionagrees withlikely_false_positive. - Alerts closed as
duplicate, and alerts still open, are left out.
The figure says how often people followed the advice, not how often the advice was right.
PUT /api/v1/ai/settings
Section titled “PUT /api/v1/ai/settings”Replaces the settings. Send every field each time.
curl -s -b ~/.sp-cookies -X PUT https://speculapro.yourbank.com.gh/api/v1/ai/settings \ -H "Content-Type: application/json" \ -H "X-Change-Reason: Pilot%20of%20the%20assistant%20on%20AML%20alerts" \ -d '{ "enabled": true, "key_source": "own", "model": "", "auto_review": false, "api_key": "sk-ant-EXAMPLE-not-a-real-key" }'| Field | Required | Meaning |
|---|---|---|
enabled |
Yes | Switch the assistant on or off |
key_source |
Yes | installation or own |
model |
No | With Claude: one of the ids in models, or empty for the default. With provider openai, google, mistral or custom: required, the model’s id as the provider names it, up to 120 characters (letters, digits and . _ : / @ -, starting with a letter or digit). It isn’t checked against the provider |
auto_review |
No | Review new alerts automatically. Needs enabled |
provider |
No | With key_source own: anthropic (the default when left out), openai, google, mistral or custom. Ignored and saved as anthropic with key_source installation |
base_url |
With custom |
The address of an OpenAI-compatible API, normally ending in /v1; calls go to <base_url>/chat/completions. At most 500 characters; a trailing / is dropped. It must be https and public unless the installation allows otherwise (SP_CONNECTOR_ALLOW_PRIVATE, SP_CONNECTOR_ALLOW_HTTP), with no user name or password, no ?query and no #fragment. A path is allowed and /v1 is not enforced. Sent with key_source own and any other provider, it is refused with 422; with key_source installation it is ignored |
api_key |
No | The workspace’s own key, at most 400 characters. Write-only: it is stored encrypted and never returned. Leave the field out to keep the stored key; send an empty string to remove it. Required when the workspace is on its own key and provider or base_url changes |
The answer is 200 with enabled, key_source, model, auto_review, provider and, for
custom, base_url as saved.
| Answer | Meaning |
|---|---|
422 the key is the installation's or the workspace's own |
key_source is neither installation nor own |
422 "…" is not a model this installation offers |
With key_source installation: model isn’t in models, or the installation’s key is for a provider other than Claude (send it empty) |
422 "…" is not a Claude model the assistant offers |
With key_source own and provider anthropic: model isn’t in models |
422 "…" is not a provider the assistant can use |
An unknown provider. A value outside the list is normally refused earlier, as 422 validation failed |
422 give the model's id as the provider names it (letters, digits and . _ : / @ -) |
A provider other than anthropic with no model, or one with other characters |
422 give the API's address, such as https://models.example.com/v1 |
provider custom and base_url is missing or has no host |
422 the API's address must start with https:// |
Another scheme. Plain http is accepted only where the installation allows it (SP_CONNECTOR_ALLOW_HTTP) |
422 the API's address can't carry a user name or password; the key goes in its own field |
https://user:secret@… |
422 the API's address can't carry a ? or a #: it is the base the calls are added to, usually ending in /v1 |
A query string or a fragment |
422 the API's address is on a network this installation does not let workspaces reach |
An internal or private host, unless the installation allows it (SP_CONNECTOR_ALLOW_PRIVATE). The host is checked again each time it is dialled |
422 an API address is only for another OpenAI-compatible API |
base_url sent with key_source own and a provider other than custom |
422 the stored key was given for another provider or address; enter the key for this one |
On the workspace’s own key, provider or base_url differs from what is saved and no api_key was sent |
422 this installation has no AI key of its own; use your workspace's key |
key_source is installation and the installation has no key |
422 automatic review needs the assistant switched on |
auto_review is true with enabled false |
422 give the workspace's API key to switch the assistant on with it |
enabled with key_source own, and no key stored or sent |
The key is not tried when you save it. A key the provider refuses shows up on the first review, as
502.
When approval is required
Section titled “When approval is required”A change falls under the action type ai.settings (AI assistant), which requires a second
person by default, when at least one of these is true:
enabledgoes fromfalsetotrue;auto_reviewgoes fromfalsetotrue;key_sourcechanges;providerchanges, orbase_urlchanges whileprovideriscustom: either sends data somewhere else;modelchanges;- the request carries
api_key(any value, including an empty string that removes the key).
The last four count whether the assistant is on or off: they decide what the next switch-on would
use. A request that does nothing but switch the assistant off, stop automatic review, or both,
applies at once and answers 200, with no reason needed.
For a gated change, PUT answers as every gated endpoint does:
- Without a reason:
428 Reason required. The settings are checked first, so invalid ones get their422here. - With an
X-Change-Reasonheader:202 Accepted, with the pending request inchange_request. Nothing has changed yet. Someone else withsettings.approveapproves it, and the settings are saved then. A key sent with the request is stored encrypted while it waits.
See Change requests API for the full exchange, polling and expiry. The
request’s summary names the provider when it isn’t Claude, and the address for custom: AI
assistant on, using the workspace’s own API key for custom at https://models.example.com/v1.
POST /api/v1/ai/allowance-requests
Section titled “POST /api/v1/ai/allowance-requests”Asks the service provider for more calls on the installation’s key for the current calendar month. Nothing changes until the provider answers: calls it adds are available at once and do not carry over. It is not a settings change and needs no approval.
curl -s -b ~/.sp-cookies -X POST https://speculapro.yourbank.com.gh/api/v1/ai/allowance-requests \ -H "Content-Type: application/json" \ -d '{ "calls": 500, "note": "A backlog of alerts to clear before quarter end." }'| Field | Required | Meaning |
|---|---|---|
calls |
Yes | How many more calls this month, 1 to 1,000,000 |
note |
No | Why, for whoever answers. At most 1,000 characters. The provider’s staff read it as written; they are not shown who asked |
{ "id": "01a133e9-4c63-74a3-9e24-3522780eb136", "calls": 500, "note": "A backlog of alerts to clear before quarter end.", "status": "pending", "granted_calls": 0, "decision_note": "", "created_at": "2026-10-13T06:18:09Z"}The same object is allowance_request in GET /api/v1/ai/settings. status becomes granted
(with granted_calls, which may differ from calls), declined, or expired when the request
was left unanswered at the end of its month (worked out when read; a new request can then be sent); decision_note is the
provider’s answer and decided_at when it was given.
| Answer | Meaning |
|---|---|
403 missing permission settings.manage |
The caller can’t manage settings |
409 a request for more AI calls is already waiting for an answer |
One request can wait at a time. A request left unanswered from an earlier month does not count: it reads expired and a new one can be sent (the stored row is then closed as declined, with the answer Not answered in the month it was asked for.). A waiting request can’t be withdrawn |
409 more calls can't be asked for from here: this installation's administrator sets the allowance |
No limit applies to the workspace, its plan does not include the installation’s key (limit 0), the installation runs no provider console, or it has no key. can_request_more is false |
422 |
calls or note outside the limits, or an API key instead of a signed-in person |
The audit log records ai.allowance_request with the number asked for. The provider’s answer
arrives as provider.ai_topup or provider.ai_request_declined, and as a notification to
everyone with settings.manage. A support session can’t call this endpoint.
Past the allowance, every endpoint that asks the assistant answers
429 this month's AI allowance is used up on the installation’s key.
Advice on an alert
Section titled “Advice on an alert”GET /api/v1/alerts/{id}/ai-review
Section titled “GET /api/v1/alerts/{id}/ai-review”The latest review of the alert, and whether the assistant can be asked.
curl -s -b ~/.sp-cookies \ https://speculapro.yourbank.com.gh/api/v1/alerts/0192f3d0-aaaa-7000-8000-000000000010/ai-review{ "available": true, "review": { "id": "0192f3d0-bbbb-7000-8000-000000000020", "recommendation": "likely_false_positive", "confidence": "medium", "summary": "Customer A made four cash deposits under the threshold in 48 hours. Two earlier alerts of the same kind were closed as false positives.", "reasons": ["4 cash deposits totalling 19,400.00 GHS, in line with 48,200.00 GHS received over 90 days"], "checks": ["Confirm the declared source of the cash"], "model": "claude-sonnet-5-5", "automatic": false, "requested_by": "Efua Mensah", "created_at": "2026-10-12T10:02:11Z", "out_of_date": false, "superseded": false, "withheld": false }}| Field | Type | Meaning |
|---|---|---|
available |
boolean | The assistant is switched on for this workspace. false too after a move to another region, until it is switched on again there |
review |
object | The latest review; omitted when the alert has none. A review stays readable after the assistant is switched off and after the alert is closed |
review.recommendation |
string | likely_false_positive, needs_investigation or insufficient_information |
review.confidence |
string | low, medium or high |
review.summary |
string | A few sentences, at most 1,200 characters |
review.reasons |
array | Up to six strings: what in the alert supports the recommendation |
review.checks |
array | Up to four strings: what a person could check next |
review.model |
string | The model that answered |
review.automatic |
boolean | Reviewed by the worker without being asked |
review.requested_by |
string | The name of the person who asked; omitted for an automatic review |
review.created_at |
time | When the review was made |
review.out_of_date |
boolean | The alert has had hits since this review, or superseded is true |
review.superseded |
boolean | The review says likely_false_positive and the alert is now held (its subject designated, or its severity raised to critical, since). Always with out_of_date: true. The queue and automatic review ask about such an alert again |
review.withheld |
boolean | true when the caller doesn’t hold entities.view: summary is then an empty string and reasons and checks are empty arrays, because the reasoning can mention customer details the caller may not see. The other fields are returned as usual |
People and organisations appear in the text as placeholders such as Customer A, never by name.
An alert that doesn’t exist in your workspace answers 404 not found.
POST /api/v1/alerts/{id}/ai-review
Section titled “POST /api/v1/alerts/{id}/ai-review”Asks the assistant about an open alert. There is no body. The call waits for the model, which
takes seconds; allow up to a few minutes before timing out. The answer is 200 with the review
object above (without the available wrapper).
curl -s -b ~/.sp-cookies -X POST \ https://speculapro.yourbank.com.gh/api/v1/alerts/0192f3d0-aaaa-7000-8000-000000000010/ai-reviewThe call is counted in usage before it is made. Each answer is stored as a new review and written
to the audit log as ai.alert_review. A caller without entities.view is refused with 403 and no call is made (reading a review someone
else asked for still works for them, with withheld: true). Asking again adds another review; the earlier ones are kept. Sanctions alerts,
critical alerts and alerts about subjects designated pep, pep_rca or sanctioned, or about an account owned by such a customer (held: true on the alert), are never
answered with likely_false_positive: see
The guardrail in code.
| Answer | Meaning | What to do |
|---|---|---|
403 the assistant's answer can mention customer details your role may not see (entities.view), so it is not asked on your behalf |
The caller holds cases.work but not entities.view. No call was made or counted |
Have someone who can see customers ask |
404 not found |
No such alert in your workspace | Check the ID |
409 the AI assistant is off for this workspace |
Nobody has switched it on | Switch it on under Settings → AI assistant |
409 the AI assistant has no API key to use |
The chosen key source has no key | Store your key, or choose the other source |
409 the assistant answered this a moment ago and nothing has changed since; ask again in a few minutes |
A person asked again within five minutes of a review made at the alert’s present hit count, whether a person or the worker made it | Read the review that is there. The wait ends after five minutes or with the alert’s next hit. Not counted |
409 only an open alert can be reviewed |
The alert is in a case or closed | Nothing: the earlier review, if any, stays readable |
429 this month's AI allowance is used up |
The workspace reached monthly_calls on the installation’s key |
Wait for the next calendar month, or use your institution’s own key. Not counted |
502 the AI provider refused the API key; check it under Settings, AI assistant |
The provider answered that the key is not valid or not allowed | Replace the key. The call is counted |
502 the AI provider could not answer; try again in a moment |
The provider couldn’t be reached, answered with an error, gave no answer, or answered in an unexpected form | Try again. No review was stored, but the call is counted, because the provider may bill it |
Summary of a case
Section titled “Summary of a case”A summary is for whoever picks a case up: what it is about, the facts that matter and what is still open, with a suggestion of what to do with the case. The suggestion is advice: nothing happens to the case, and closing it or filing a report stays a person’s decision. What is sent is listed under Case summaries.
GET /api/v1/cases/{id}/ai-summary
Section titled “GET /api/v1/cases/{id}/ai-summary”Permission: cases.view. Answers 404 for an unknown case.
{ "available": true, "summary": { "id": "0192f3d0-cccc-7000-8000-000000000001", "suggestion": "keep_investigating", "confidence": "medium", "suggestion_reasons": ["Seven deposits just under the threshold in eight days, and nothing in the data explains them."], "summary": "Customer A has two open AML alerts for cash deposits just under the reporting threshold…", "key_points": ["Seven deposits between 9,200.00 and 9,900.00 GHS from 2 to 9 October."], "open_questions": ["The source of the cash is not in the data."], "model": "claude-sonnet-5-5", "requested_by": "Efua Mensah", "created_at": "2026-10-12T09:14:02Z", "out_of_date": false, "superseded": false, "withheld": false }}| Field | Type | Meaning |
|---|---|---|
available |
boolean | The assistant is switched on for this workspace (false too after a move to another region, until it is switched on again there) |
summary |
object | The latest summary; omitted when the case has none |
summary.suggestion |
string | likely_false_positive, consider_report, keep_investigating or insufficient_information. Omitted on a summary written before suggestions existed. Returned to every caller, including one without entities.view |
summary.confidence |
string | low, medium or high: the model’s own estimate. Omitted with suggestion; an answer with any other value is stored as low |
summary.suggestion_reasons |
array of strings | Up to four facts from the data that support the suggestion, each at most 400 characters. When SpeculaPro replaced the suggestion (see below), its own sentence comes first, so there can be five |
summary.summary |
string | One short paragraph, at most 1,500 characters |
summary.key_points |
array of strings | Up to six facts from the data |
summary.open_questions |
array of strings | Up to four things the data doesn’t answer |
summary.model |
string | The model that answered |
summary.requested_by |
string | The person who asked. A summary is never automatic |
summary.created_at |
date-time | When it was written |
summary.out_of_date |
boolean | The case has more alerts, hits and timeline entries than when the summary was written, or superseded is true |
summary.superseded |
boolean | The summary suggests likely_false_positive and the case is now held: a sanctions case, critical, about a held subject, or holding a held alert. Always with out_of_date: true |
summary.withheld |
boolean | true for a caller without entities.view: summary is then "" and suggestion_reasons, key_points and open_questions are empty. suggestion and confidence are still returned |
The suggestion is checked in code. If the model answers likely_false_positive for a case
whose kind is sanctions, whose priority is critical, whose subject is held (a pep,
pep_rca or sanctioned designation in force on it or, for an account, on its owner), or which
holds any alert with held: true, SpeculaPro stores keep_investigating with high confidence
and puts its own sentence first in suggestion_reasons. The check runs when the summary is
written; a stored summary is not rewritten when the subject is designated later. An answer whose suggestion is not
one of the four values is refused with 502 and nothing is stored. See
What it never suggests.
Case objects from GET /api/v1/cases carry the latest
summary’s suggestion as ai_suggestion, without the staleness check.
POST /api/v1/cases/{id}/ai-summary
Section titled “POST /api/v1/cases/{id}/ai-summary”Permission: cases.work and entities.view. No body. Works on open and closed cases. Answers 200
with the summary object above. The call is written to the audit log as ai.case_summary (with the suggestion and confidence) and counted under the
feature case_summary, against the same monthly allowance as alert reviews.
| Answer | Meaning | Counted |
|---|---|---|
403 the assistant's answer can mention customer details your role may not see (entities.view), so it is not asked on your behalf |
The caller doesn’t hold entities.view. No call is made |
No |
404 not found |
No such case | No |
409 the AI assistant is off for this workspace |
Off, or its settings were saved in another region | No |
409 the AI assistant has no API key to use |
The key the settings name isn’t there | No |
409 the assistant answered this a moment ago and nothing has changed since; ask again in a few minutes |
A summary less than five minutes old exists and the case has had no alert, hit or timeline entry since | No |
429 this month's AI allowance is used up |
The workspace reached monthly_calls on the installation’s key |
No |
502 the AI provider refused the API key; check it under Settings, AI assistant |
The provider didn’t accept the key | Yes |
502 the AI provider could not answer; try again in a moment |
The provider couldn’t be reached or answered in an unexpected form. No summary was stored | Yes |
Draft of a report’s narrative
Section titled “Draft of a report’s narrative”The assistant drafts the narrative of an STR or SAR that is still a draft and was made from a
case. The draft is a suggestion kept beside the report: neither endpoint changes the report. To
use a draft, put its text in reason with PATCH /api/v1/reports/{id}, as for
any narrative. What is sent is listed under
Report narrative drafts.
Both endpoints need reports.draft and entities.view_pii, like every other report endpoint,
because a draft names the customer. Without entities.view_pii the answer is 403 reports contain customer identity; you need entities.view_pii.
GET /api/v1/reports/{id}/ai-narrative
Section titled “GET /api/v1/reports/{id}/ai-narrative”{ "available": true, "draft": { "id": "0192f3d0-dddd-7000-8000-000000000001", "narrative": "This report concerns Kwame Asante, a retail customer. Between 2 and 9 October 2026 seven cash deposits were made to account [number]…", "gaps": ["The customer's stated source of the cash is not in the data."], "model": "claude-sonnet-5-5", "requested_by": "Efua Mensah", "created_at": "2026-10-13T09:14:02Z" }}| Field | Type | Meaning |
|---|---|---|
available |
boolean | A draft can be asked for: the assistant is on for this workspace, and the report is an STR or SAR, still a draft, made from a case |
draft |
object | The latest draft; omitted when the report has none. A report’s drafts are deleted when it leaves draft (sent for approval, or voided), so from then on the answer is available: false with no draft |
draft.narrative |
string | The suggested text, at most 4,000 characters, with the names put back. Identifying numbers were never sent and appear as [number] |
draft.gaps |
array of strings | Up to six things the model could not know and a person must add |
draft.model |
string | The model that answered |
draft.requested_by |
string | The person who asked. A draft is never automatic |
draft.created_at |
date-time | When it was written |
Answers 404 not found for an unknown report.
POST /api/v1/reports/{id}/ai-narrative
Section titled “POST /api/v1/reports/{id}/ai-narrative”No body. Answers 200 with the draft object above. The call is written to the audit log as
ai.narrative_draft on the report (never the text) and counted under the feature
narrative_draft, against the same monthly allowance as alert reviews. Each request stores a new
draft; the earlier ones are kept and the latest is returned by GET.
| Answer | Meaning | Counted |
|---|---|---|
403 reports contain customer identity; you need entities.view_pii |
The caller holds reports.draft but not entities.view_pii |
No |
404 not found |
No such report | No |
409 the assistant drafts the narrative of an STR or SAR that is still a draft and was made from a case |
The report is a CTR, has left draft, or was not made from a case | No |
409 the AI assistant is off for this workspace |
Off, or its settings were saved in another region | No |
409 the AI assistant has no API key to use |
The key the settings name isn’t there | No |
409 the assistant answered this a moment ago and nothing has changed since; ask again in a few minutes |
The report’s latest draft is less than five minutes old, whoever asked for it. Unlike alerts and cases, nothing lifts the wait early | No |
429 this month's AI allowance is used up |
The workspace reached monthly_calls on the installation’s key |
No |
502 the AI provider refused the API key; check it under Settings, AI assistant |
The provider didn’t accept the key | Yes |
502 the AI provider could not answer; try again in a moment |
The provider couldn’t be reached, or answered in an unexpected form or with an empty narrative. No draft was stored | Yes |
Tuning suggestions for a rule
Section titled “Tuning suggestions for a rule”The assistant reads a rule’s live version and counts of what became of its alerts over the
last 90 days, and answers with an assessment and up to four changes worth testing. Neither endpoint
changes the rule: a suggestion is tried with the Backtesting API and becomes a
new version only through POST /api/v1/rules/{id}/versions and its approval. What
is sent is listed under Rule tuning suggestions:
the rule and counts, no customer.
GET /api/v1/rules/{id}/ai-tuning
Section titled “GET /api/v1/rules/{id}/ai-tuning”Needs rules.view.
{ "available": true, "advice": { "id": "0192f3d0-eeee-7000-8000-000000000001", "assessment": "Nine of the 14 decided alerts were closed as false positives…", "suggestions": [ { "change": "Test min_count at 4 instead of 3.", "why": "Among the hits whose alerts were closed as false positives, cash_in_count_48h has a median of 3.", "risk": "The earliest hits of alerts that went to cases would no longer fire." } ], "cautions": ["14 decided alerts is a small sample."], "rule_version": 3, "model": "claude-sonnet-5-5", "requested_by": "Kwesi Appiah", "created_at": "2026-10-13T09:14:02Z", "out_of_date": false }}| Field | Type | Meaning |
|---|---|---|
available |
boolean | The assistant is on for this workspace, the rule has a live version, and its category is not designations (the rule packs’ rules about sanctioned and politically exposed parties), sanctions or screening. It doesn’t say whether the rule has enough history: that is checked when you ask |
advice |
object | The latest answer; omitted when the rule has none |
advice.assessment |
string | How the rule is doing, at most 1,200 characters |
advice.suggestions |
array | Up to four objects with change (what to test), why (the figures behind it) and risk (what it could miss). Empty when the figures support no change |
advice.cautions |
array of strings | Up to four things the figures cannot show |
advice.rule_version |
integer | The live version number that was read |
advice.model |
string | The model that answered |
advice.requested_by |
string | The person who asked. Never automatic |
advice.created_at |
date-time | When it was written |
advice.out_of_date |
boolean | The rule’s live version number is now higher than rule_version |
Answers 404 not found for an unknown rule. A support session with the
configuration scope can read this endpoint.
POST /api/v1/rules/{id}/ai-tuning
Section titled “POST /api/v1/rules/{id}/ai-tuning”Needs rules.author. No body. Answers 200 with the advice object above. The call is written to
the audit log as ai.rule_tuning on the rule and counted under the feature rule_tuning, against
the same monthly allowance as the other features. Each request stores a new answer; GET returns
the latest. A support session can’t call it.
| Answer | Meaning | Counted |
|---|---|---|
404 not found |
No such rule | No |
409 the rule has no active version to look at |
Nothing is live for this rule | No |
409 the assistant does not suggest changes to sanctions rules: what they must catch is set by law, not by how past alerts were closed |
The rule’s category is designations, sanctions or screening |
No |
409 fewer than 10 of this rule's alerts in the last 90 days have been decided: too few to suggest anything |
Fewer than ten alerts with hits of this rule in the period have left the queue | No |
409 the AI assistant is off for this workspace |
Off, or its settings were saved in another region | No |
409 the AI assistant has no API key to use |
The key the settings name isn’t there, or the workspace’s plan doesn’t include the installation’s key | No |
409 the assistant answered this a moment ago and nothing has changed since; ask again in a few minutes |
The latest answer is about the same live version (or a newer one) and is less than five minutes old | No |
429 this month's AI allowance is used up |
The workspace reached its monthly allowance on the installation’s key | No |
502 the AI provider refused the API key; check it under Settings, AI assistant |
The provider didn’t accept the key | Yes |
502 the AI provider could not answer; try again in a moment |
The provider couldn’t be reached, or answered in an unexpected form or without an assessment. Nothing was stored | Yes |
Batches
Section titled “Batches”A batch asks the assistant about many alerts or cases at once. It queues the work and returns at once; the worker then takes the items four at a time. Each answer lands where a single answer does (the alert’s review, the case’s summary), is audited and counted like a single one, and changes nothing on the alert or case. A batch never closes anything.
POST /api/v1/ai/batches
Section titled “POST /api/v1/ai/batches”Permission: cases.work and entities.view.
| Field | Required | Values |
|---|---|---|
kind |
Yes | alert_review (advice on open alerts) or case_summary (a summary and suggestion on cases) |
ids |
One of the two | Up to 200 alert or case IDs. Repeated IDs are queued once |
unadvised |
One of the two | true: take everything without current advice, up to 200 |
Send ids or unadvised: true, never both and never neither.
With unadvised, the batch takes:
- for
alert_review: open alerts with no review, or with hits since their latest review, most severe first (critical, high, medium, low), then the most recent activity; - for
case_summary: cases that are not closed and not awaiting approval and have never been summarised, by priority (critical first), then the soonest deadline. A case whose summary is out of date is not taken.
An alert or case already waiting in another batch is left out.
curl -s -b ~/.sp-cookies -X POST https://speculapro.yourbank.com.gh/api/v1/ai/batches \ -H 'Content-Type: application/json' -d '{"kind":"alert_review","unadvised":true}'{ "id": "0192f3d0-ffff-7000-8000-000000000001", "kind": "alert_review", "total": 40, "queued": 40, "running": 0, "done": 0, "skipped": 0, "failed": 0, "finished": false, "created_at": "2026-10-13T09:14:02Z"}| Field | Type | Meaning |
|---|---|---|
id |
UUID | The batch |
kind |
string | alert_review or case_summary |
total |
integer | How many alerts or cases the batch holds |
queued, running |
integer | Waiting, and being read now |
done |
integer | Answered |
skipped |
integer | Not asked: it already had current advice, was no longer open, was not found, was answered less than five minutes ago, or nothing more could be asked (see last_reason). A skipped item is not counted against the allowance, except the one item on which the provider refused the key |
failed |
integer | The call was made and failed. Counted against the allowance like any failed call |
last_reason |
string | Why the latest item was skipped or failed; omitted when none was |
finished |
boolean | Nothing is waiting or running |
created_at |
time | When the batch was asked for |
| Answer | Meaning |
|---|---|
403 missing permission cases.work |
The caller can’t work cases |
403 the assistant's answer can mention customer details your role may not see (entities.view), so it is not asked on your behalf |
The caller doesn’t hold entities.view |
409 the AI assistant is off for this workspace, 409 the AI assistant has no API key to use |
Said at once, not on every item |
409 there is nothing to ask the assistant about: everything in the queue already has its advice, or is already being looked at |
Nothing was queued: with unadvised, nothing lacks advice; with ids, every one that could be asked about is already waiting in a batch |
422 none of the alerts or cases named can be asked about: they are not in this workspace, or the alerts are no longer open |
With ids: none of them is an open alert (or a case) of this workspace. When only some are, those are left out without a message and total is smaller than the list sent |
422 name up to 200 alerts or cases, or ask for everything without advice |
Both ids and unadvised, neither, or more than 200 IDs |
How a batch runs:
- The allowance is checked on each item, not when the batch starts. A batch of 200 can use 200
calls. When the allowance runs out, the assistant is switched off, the key is missing or the
provider refuses the key, the items still waiting are marked
skippedwith that reason. - Three provider failures in a row stop a batch. The items still waiting are
skippedwiththe AI provider failed three times in a row: …. The failed calls are counted. Each worker process keeps its own count in memory, so with several workers a batch can fail a few more times before it stops. - Named IDs are filtered when the batch is queued. Only open alerts (for
alert_review) or existing cases (forcase_summary) of this workspace are queued; the others are left out silently, sototalcan be smaller than the number of IDs sent. - Each answer is recorded as asked by the person who started the batch. Before each item the
worker checks that this person’s account is still active and still holds
cases.workandentities.view; if not, the rest isskippedwiththe person who asked may no longer ask the assistant. Signing out does not stop a batch. - A workspace that is suspended or being moved to another region has its batches paused.
- A worker that is stopping still records what became of the items it held. An item left
runningby a worker that died is taken again after ten minutes: if the first attempt had called the provider but not stored the answer, the second attempt is a second counted call; if the answer was stored, the second attempt is skipped as already advised. - Finished items are deleted seven days after they finish; after that the batch answers
404.
GET /api/v1/ai/batches/{id}
Section titled “GET /api/v1/ai/batches/{id}”Permission: cases.work. The batch object above: counts only, no alert or case IDs. A batch is
readable only by the person who started it.
404 not found for an unknown batch, someone else’s batch, or one whose items have all been deleted.
GET /api/v1/ai/batches?kind=
Section titled “GET /api/v1/ai/batches?kind=”Permission: cases.work. kind is required. Returns the caller’s most recent batch of that
kind as {"batch": {…}}, or {} when they have asked for none in the last week. The queue pages
use it to show progress again after a reload.
GET /api/v1/ai/available
Section titled “GET /api/v1/ai/available”Permission: alerts.view or cases.view. Returns {"available": true} when the assistant is
switched on for the workspace, and nothing else about its settings.
On alerts
Section titled “On alerts”Alert objects from GET /api/v1/alerts and
GET /api/v1/alerts/{id} carry ai_recommendation when the assistant has advised on the alert
since its last hit. It is omitted when there is no review, when the review is out of date, and
when the review says likely_false_positive but the alert is now held.
GET /api/v1/alerts takes ai= to list only alerts with a given current recommendation, or
ai=none for alerts with no current advice. See
GET /api/v1/alerts.
Closing alerts is never done by the assistant. A person can close several at once with
POST /api/v1/alerts/close.