Read one profile
https://api.granska.cloud/v1/profiles/:idReads one profile in full — every granskare it runs, and every legal source each of those holds.
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.
curl https://api.granska.cloud/v1/profiles/lss_utredning \
-H "Authorization: Bearer $TOKEN"{
"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 instead is that your
organisation gets its own copy — written under your prefix, carrying whatever you sent — and the
profile is repointed at the copy. The response is 200 and names nothing of this. workers replaces
the whole set, so this happens whether you changed that reviewer or merely sent it back untouched
alongside the one you did change.
From then on the profile runs your copy and our own updates of the original no longer reach it, which is sometimes exactly what you want and is not something you can undo through this API. Two things to plan for:
- A profile that was tracking our reviewer stops tracking it. If you want to keep tracking it,
do not edit the profile's
workersat all — there is no way to send one reviewer and leave the others as references. - The copy is weighed as a new reviewer. A reviewer of ours holding more legal sources than your
workspace's ceiling allows is fine where it stands, but the copy has nothing stored to be measured
against, so the edit can be refused with
too-many-legal-sourcesnaming a reviewer you never touched. Either drop sources from it or ask us to raise the ceiling.
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, so a Swedish
authority applying GDPR is ["SE", "EU"].
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.
| Error | When |
|---|---|
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. |
404NOT_FOUND | No profile with that id, or none this tenant is licensed for. The gateway does not distinguish the two. |
500INTERNAL_ERROR | An unexpected server-side failure. Not probable from outside — reaching it means something is wrong. |