GRANSKA

Run an action

POSThttps://api.granska.cloud/v1/action

Queues a follow-up action over a finished analysis and answers immediately with a job id. Consumes one run from the tenant's quota.

Bearer tokenSpends 1 run — even when the request is rejectedAnswers with the rate-limit headers

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.

Request
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": [] }
  }'
Response
{
  "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.

ParameterDescription
clinicalData
Reducer·body·required
The finished analysis to act on, exactly as GET /v1/jobs/:jobId returned it.
actionType
string·body·required
Which action to run. GET /v1/actions lists the ones this tenant is licensed for.
profileId
string·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.
includeDiagnostics
boolean·body
Adds the action job's diagnostic output to the finished job.
webhookUrl
string·body
Called when the job finishes, instead of polling GET /v1/jobs/:jobId.
webhookSecret
string·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

ErrorWhen
400
BAD_REQUEST
clinicalData or actionType is missing, clinicalData failed validation, or the action type is not one this tenant can run.
401
UNAUTHORIZED
The Authorization header is missing, is not a readable bearer token, or names no tenant.
401
TOKEN_EXPIRED
The access token was issued by this gateway and has since expired. Not probed: it needs a token older than its own lifetime.
403
FORBIDDEN
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.
429
TOO_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.
500
INTERNAL_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.