Run an action
https://api.granska.cloud/v1/actionQueues a follow-up action over a finished analysis and answers immediately with a job id. Consumes one run from the tenant's quota.
An action turns a finished audit into a document — the grounds for an appeal, a plain-language
summary, a letter. You send back the audit you were given, name the action, and get a jobId; the
work is asynchronous exactly as POST /v1/analyze is, and you collect the result
from GET /v1/jobs/:jobId under result.generatedDocument.
A run is spent before the request is validated. As on the analysis call, the counter increments
before the handler looks at the body, so a 400 here has still cost a run.
curl -X POST https://api.granska.cloud/v1/action \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"actionType": "action_overklagande",
"profileId": "lss_utredning",
"clinicalData": { "summary": "…", "findings": [] }
}'{
"success": true,
"jobId": "job_b2e08a",
"status": "QUEUED"
}Request body
clinicalData is the audit, fed back in verbatim — the object
GET /v1/jobs/:jobId returned as result.clinicalData. actionType is an id
from GET /v1/actions.
profileId is required — the same id you passed to POST /v1/analyze to
produce the audit. A request without it is a 400. It used to be optional, and the engine then
worked out which profile produced the audit by matching the document type: an organisation with more
than one profile for the same document type could silently land on the wrong one, which means the
wrong legal framework and possibly the wrong output language. Named explicitly, the profile is
validated against your licences the same way the analysis call validates it — and
GET /v1/actions?profileId= lists the actions that profile is written for,
so the actionType you send here can be chosen from that list rather than from all of them.
Fields the audit schema does not recognise are ignored, so an audit you have annotated on your own side can be sent back as it stands.
| Parameter | Description |
|---|---|
clinicalDataReducer·body·required | The finished analysis to act on, exactly as GET /v1/jobs/:jobId returned it. |
actionTypestring·body·required | Which action to run. GET /v1/actions lists the ones this tenant is licensed for. |
profileIdstring·body·required | The profile the analysis was produced with — the same id you passed to POST /v1/analyze. Omitting it is a 400.Required since #681, and this row said otherwise until #1061. What it used to describe — omitting it and letting the gateway match on document type — matched by a non-unique label and could hand the action a different profile, in a different language, citing another country's law, than the analysis had used. GET /v1/actions?profileId= lists the actions this profile is written for. |
includeDiagnosticsboolean·body | Adds the action job's diagnostic output to the finished job. |
webhookUrlstring·body | Called when the job finishes, instead of polling GET /v1/jobs/:jobId. |
webhookSecretstring·body | Signs the webhook call with HMAC-SHA256 so the receiver can verify it came from here. |
An audit stored before the citation rework is rejected
clinicalData is validated against the same schema an analysis produces. A result saved before the
citation fields were reworked lacks primary_authority_type and primary_label, both required, and
the call answers 400 BAD_REQUEST with the validation error in message.
There is no translation path, and there will not be one. primary_label has to be a label the
engine issued during the very analysis that produced the finding, and those labels no longer exist
for an analysis that has been delivered and deleted. An old result cannot be recomputed into the new
shape — it can only be produced again.
What to do: run the document through POST /v1/analyze again and send the fresh
result here. If you hold a queue of saved audits waiting for action generation, drain it before
upgrading. A result produced after the rework can be sent back unchanged, as always — the break is
across one release boundary, in one direction, once.
The same applies to the stored results in
GET /v1/examples/:profileId, which are also from before the rework.
{
"error": {
"code": "BAD_REQUEST",
"message": "Invalid clinicalData: ..."
}
}
Diagnostics and webhooks
includeDiagnostics adds this action's own diagnostic output — the model, the action type, the agent
name, the generator's raw output and token usage — under result.diagnostics on the finished job. It
is scoped to this action and is not the broader internal debug bundle. It works in production.
webhookUrl and webhookSecret behave exactly as they do on the analysis call, down to the payload,
the retry schedule and the signature. See
Webhooks.
Errors
| Error | When |
|---|---|
400BAD_REQUEST | clinicalData or actionType is missing, clinicalData failed validation, or the action type is not one this tenant can run. |
401UNAUTHORIZED | The Authorization header is missing, is not a readable bearer token, or names no tenant. |
401TOKEN_EXPIRED | The access token was issued by this gateway and has since expired. Not probed: it needs a token older than its own lifetime. |
403FORBIDDEN | The tenant has no configuration, or is not licensed for the action or profile it named. Not probed: it needs a licence the probing tenant deliberately lacks. |
429TOO_MANY_REQUESTS | The tenant has spent its runs for the current period. GET /v1/quotas says when the period resets. Not probed: it would have to spend a real quota to reach. |
500INTERNAL_ERROR | An unexpected server-side failure. Not probable from outside — reaching it means something is wrong. |
Send it without writing a client
If your organisation already has an account, an administrator can send this call from the API tester
at /admin/api-tester. It runs against your own tenant and spends a real run.