GRANSKA

Read one profile

GEThttps://api.granska.cloud/v1/profiles/:id

Reads one profile in full — every granskare it runs, and every legal source each of those holds.

Bearer tokenSpends no quota

What this answers that the list does not

GET /v1/profiles tells you which audits you may run and which reviewers are inside each one. This tells you what those reviewers contain: the brief each one was written with, every statute provision it enforces, and every legal source it holds — in the same spelling POST /v1/profiles accepts them in.

That is what makes an edit safe. workers on PATCH /v1/profiles/:id replaces the whole set of reviewers, so changing one reviewer's provisions while leaving the others alone means sending the others back unchanged. Read them here, change the one you meant to change, and send the array back.

The list endpoint is untouched and stays cheap. This one reads a record per reviewer, so call it when you need the contents, not to build a picker.

Request
curl https://api.granska.cloud/v1/profiles/lss_utredning \
  -H "Authorization: Bearer $TOKEN"
Response
{
  "profile": {
    "id": "lss_utredning",
    "name": "LSS-utredning",
    "documentType": "LSS",
    "categoryPath": "Funktionsstöd",
    "tenantId": "SYSTEM",
    "status": "PUBLISHED",
    "unlisted": false,
    "jurisdictions": [
      "SE"
    ],
    "asOf": "Rättsläget 2019",
    "userHelpText": "För utredningar om insatser enligt LSS.",
    "urlSlug": "lss-utredning",
    "outputLanguage": "Swedish",
    "updatedAt": "2026-08-04T09:12:44.000Z",
    "workers": [
      {
        "id": "SYSTEM_worker_legal",
        "name": "Rättslig grund",
        "instructions": "Weigh the investigation against the conditions for the measure applied for.",
        "shared": true,
        "legalSourceCount": 3,
        "rules": [
          {
            "jurisdiction": "SE",
            "work": "1993:387",
            "pinpoint": "par_7§"
          },
          {
            "jurisdiction": "SE",
            "work": "1993:387",
            "pinpoint": "par_9a§"
          }
        ],
        "snippetIds": [
          "lss_insatser_allmanna_rad"
        ]
      },
      {
        "id": "SYSTEM_worker_objectivity",
        "name": "Objektivitetsgranskare",
        "instructions": "Look for value-laden wording and conclusions the document does not support.",
        "shared": false,
        "legalSourceCount": 1,
        "rules": [
          {
            "jurisdiction": "SE",
            "work": "2017:900",
            "pinpoint": "kap_5_par_1§"
          }
        ],
        "snippetIds": []
      }
    ]
  }
}

What a reviewer carries

rules are provisions of published law, each { jurisdiction, work, pinpoint } — work is which law, pinpoint is where in it, and both are opaque strings we never parse. snippetIds are the keys of authored legal sources, which GET /v1/snippets lists. The two are different kinds of identifier and are never mixed.

legalSourceCount is rules.length + snippetIds.length. It is redundant beside the arrays on purpose: it is the number our own write path caps a reviewer against, so it is what to check your array lengths against after an edit.

instructions is what the reviewer was told to look for — one stored string, written by whoever built the reviewer, here or in our own authoring interface. It is not the prompt the model receives. That prompt is assembled when a job runs, out of these instructions plus the engine's own rules for citation, evidence and output, and none of it is stored or published.

shared means another of your profiles names the same reviewer. Editing it changes what that other profile runs, and nothing in the request or the response will mention it.

id on a reviewer is longer than the profile's own id and carries an organisation prefix. That is not something to trim: send it back exactly as it appears here. The prefix also tells you who owns the reviewer, which decides what sending it back does — see A reviewer we own below.

A reviewer the profile names but that no longer resolves to a record is left out of this response entirely, rather than answered as an empty one. It is left out of the profile too if you send the array back: workers replaces the whole set, so a reference that has gone missing is dropped by the next edit you make.

Sending it back

shared and legalSourceCount are computed for this response — they are not stored on the reviewer and there is nothing to write them to. Drop those two before sending workers to PATCH /v1/profiles/:id; the edit endpoint accepts id, name, instructions, rules and snippetIds, and refuses anything else with a 400 naming the field rather than ignoring it. Everything else comes back in the shape it goes out in, so the edit is: read, drop the two, change the one reviewer you meant to change, send the whole array back.

A reviewer we own

Your own profile may name a reviewer that belongs to us rather than to you — that is what a profile copied from one of ours starts out as, and its id says so: SYSTEM_ and the template prefixes are ours, your own reviewers carry your organisation's id.

Sending one of those back to PATCH /v1/profiles/:id does not edit it, and cannot: other organisations run the same record. What happens depends on whether you changed it.

  • Sent back as it stands, it stays ours and the profile keeps tracking it. Nothing is written, the profile goes on naming our reviewer, and our later improvements to it keep reaching your analyses. That is what makes the ordinary edit expressible: read the array, change the one reviewer you meant to change, send the whole array back. workers replaces the whole set, and a reviewer of ours that is in the set it replaces comes through untouched.
  • Sent back with any of name, instructions, rules or snippetIds changed, the request is refused with 403 FORBIDDEN and inherited-record-readonly, naming the reviewer. This also applies when its id names the template's own record. An omitted field is read as the empty value, because this endpoint replaces the whole reviewer — so { "id": …, "name": … } alone is a request to empty its instructions and legal sources, and is refused unless they are empty already.

To run your own version of one of our reviewers, build it as a new reviewer — send it with no id and it is created under your organisation, as a record of its own that you own and can edit. It stands beside ours rather than replacing it, and the profile can name either.

Until 2026-09 a changed reviewer of ours was answered 200 and your organisation silently got a private copy under your own prefix, which replaced our record for your whole workspace: every profile of yours that named that reviewer ran the frozen copy from then on, and nothing said so. That is the write the refusal above closes.

The profile's own fields

Everything the list endpoint answers, plus three the list does not carry.

jurisdictions are the legal orders analyses under this profile apply — a set of codes, each two upper-case letters with an optional _ suffix. Which codes are open is a catalogue the platform's administrators keep, not a list this page could carry; the legal orders your organisation may use are in its settings.

A set is not the same as several. A Swedish authority applying GDPR declares ["SE"] alone: EU instruments reach the analysis through Sweden's own norm hierarchy, which ranks them above ordinary statute, so there is no second code to pair with the first. A second code belongs here when the analysis genuinely applies two legal orders, and not to reach law that one of them already carries.

asOf is a label, and only a label. It records that an author deliberately pinned this profile to an older wording of the law — "Rättsläget 2019" — so that a profile which is meant to be historical can be told apart from one that is merely out of date. There is no resolution behind it: nothing computes "the version in force on that date". The field is absent when no author set one.

outputLanguage is the language analyses under this profile answer in, where the profile sets one.

updatedAt is when the owning record was last written, in ISO 8601, and is a caching hint rather than a freshness guarantee. It is absent on records written before we started stamping.

Errors

A profile your tenant is not licensed for answers 404, exactly as an id that does not exist does — the two are deliberately indistinguishable, so this endpoint cannot be used to find out whether another organisation holds a profile. Note the divergence from POST /v1/analyze, which answers 403 for an unlicensed profile: the two endpoints on this path — reading and editing — agree with each other instead.

ErrorWhen
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.
404
NOT_FOUND
No profile with that id, or none this tenant is licensed for. The gateway does not distinguish the two.
500
INTERNAL_ERROR
An unexpected server-side failure. Not probable from outside — reaching it means something is wrong.