GRANSKA

Edit a profile

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

Edits a profile this tenant owns, and publishes or unpublishes it. What the request omits keeps the value it had, status included.

Bearer tokenSpends no quotaAnswers with the rate-limit headers

What an edit changes

This is the endpoint that changes what your audits actually do. A profile created here arrives as a draft nobody is using yet; an edit lands on a profile people may be running today, and it takes effect on the next analysis that starts. It is also where a draft you built is published — see below.

Send only the fields you want changed. Anything you leave out keeps the value it had, including the reviewers — omit workers entirely and only the profile record is touched.

The :id in the path is the profile's id as GET /v1/profiles lists it, which is the same string POST /v1/profiles hands back and the same one POST /v1/analyze takes. There is one id per profile and it works on every route. An id your organisation does not hold is a 404, which says nothing about whether it exists somewhere else.

Request
curl -X PATCH https://api.granska.cloud/v1/profiles/lss_utredning_k4m2 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "userHelpText": "Use this profile for investigations decided after 1 January 2026."
  }'
Response
{
  "success": true,
  "profileId": "lss_utredning_k4m2",
  "workerIds": [
    "tenant_9f3a_worker_rattslig_grund_p8x1"
  ],
  "status": "PUBLISHED"
}

Publishing, and staying published

status is what decides whether your organisation's own people are offered the profile in their list. Send "PUBLISHED" to publish it and "DRAFT" to take it back to a draft. This is the call that finishes a profile you built over the API: nobody has to open the admin panel for it.

Omit status and the stored one stands. An edit that renames a published profile leaves it published, and one that reworks a draft leaves it a draft. That is deliberate, and it is the reason the field has to be sent rather than derived: the alternative — taking the profile out of circulation on every edit — made profiles disappear from people's lists with nothing to say why.

It follows that a half-finished wording reaches everyone using a published profile the moment you save it. If that is not what you want, take the profile back to "DRAFT" first, edit, and publish again.

Only "DRAFT" and "PUBLISHED" are accepted; anything else is a 400 naming them, rather than a 200 for a profile that did not move. Creating a profile does not take this field at all — a profile is created as a draft and published here afterwards.

Sending workers replaces the whole set

workers is not merged reviewer by reviewer. The array you send is the profile's reviewers afterwards:

  • A reviewer with an id you already have is edited in place.
  • A reviewer with no id is created.
  • A reviewer you leave out is no longer part of this profile. It is not deleted — if another profile names it, that profile is unaffected.

A reviewer can belong to more than one profile. GET /v1/profiles marks such a reviewer with "shared": true, and editing it here changes every profile that names it — including profiles this request never mentioned. Read the list before you edit if you did not build the profile yourself.

Sending rules on a reviewer replaces its legal sources the same way. A pinpoint is the id the provision is addressed by (kap_6_par_1§), never the citation it is printed as (6 kap. 1 §), and sending the wrong one is refused naming the reference you wrote — creating a profile explains where to get the right one.

Changing the shortcut

urlSlug is the address the profile opens at, as /p/{urlSlug}. Omit it and the stored one stands; send a new one to move the profile to a new address, or send "" to take the shortcut away. Anyone holding the old link lands on nothing afterwards, so treat it as you would any published URL.

Hiding a profile, and putting it back

unlisted: true takes the profile out of the lists your organisation's own people choose from, leaving it reachable on its /p/{urlSlug} shortcut and through this API. unlisted: false puts it back. Omit the field and the stored setting stands — an edit that renames a hidden profile leaves it hidden.

Anything that is not true or false is a 400. That matters more here than on a create: a value we could not read would look exactly like a field you never sent, so you would be answered 200 by a profile that is still hidden.

Listing is not publishing. A profile can be published and unlisted at the same time, which is precisely the profile that answers on its shortcut and appears in nobody's picker. The two fields are set independently, and sending one says nothing about the other.

Request body

ParameterDescription
name
string·body
A new name for the profile. Omit to keep the stored one.
documentType
string·body
A new document type. Omit to keep the stored one.
workers
Worker[]·body
Replaces the whole set of workers this profile runs. Omit it and the workers are left untouched.Send workers[].id to edit an existing worker, or omit it to create one. Each rules[] entry is { jurisdiction, work, pinpoint }, where pinpoint is the id the provision is addressed by ("par_7§") rather than the label it is printed as ("7 §"). A worker may be shared with another profile — GET /v1/profiles marks it — and editing a shared worker changes every profile that names it.
categoryPath
string·body
Where the profile is filed in the workspace's own grouping.
userHelpText
string·body
The help text shown beside the profile to the people who choose it.
urlSlug
string·body
A new shortcut this profile answers on, as /p/{urlSlug}. Omit to keep the stored one; send "" to remove it. Unique within your organisation: a shortcut another of your profiles already holds is a 409.
unlisted
boolean·body
Whether the profile is kept out of the pickers your organisation's own people choose from. Send true to hide it, false to list it again. Omit to keep the stored setting.Sending anything but true or false is a 400 — a value the route cannot read would otherwise leave the profile as visible as it was while the answer said 200.
outputLanguage
string·body
The language this profile's reports are written in — one of Swedish, Norwegian (Bokmål), Danish. Omit to keep the stored setting.A closed set since #820. A value outside it is a 400 naming the accepted values.
status
string·body
Whether the profile is offered to your organisation's own people. Send "PUBLISHED" to publish it, "DRAFT" to take it back to a draft. Omit it and the stored status stands — an edit that says nothing about status leaves a published profile published.Only "DRAFT" and "PUBLISHED" are accepted; anything else is a 400 naming them. Publishing is not listing: a published profile with unlisted: true is still kept out of the pickers. POST /v1/profiles does not take this field — a profile is created as a draft and published here afterwards.

An analysis already running uses the edited profile

Analyses are not frozen at the moment you start them. The profile is read when the analysis actually runs, which can be seconds or minutes after POST /v1/analyze answered — so an analysis queued before your edit will run under the profile as it is after it.

Usually that is invisible and harmless. Two edits will fail such an analysis outright: removing a reviewer it was going to run, and pushing the profile past your organisation's reviewer limit. The job then comes back from GET /v1/jobs/:jobId as FAILED with "errorCategory": "INVALID_CONFIGURATION", which is the one failure category that says the fault is in the configuration rather than in our systems — a retry of the same document will fail the same way until the profile is fixed.

If your integration edits profiles on a schedule, the safe habit is to edit when nothing of yours is in flight. Nothing stops you doing otherwise, and nothing ever silently changes an analysis you have already been given a result for.

What will be refused

  • An id that names no profile of yours. A 404, and the id to check it against is the one GET /v1/profiles lists — the same string POST /v1/analyze takes.

  • A profile your organisation does not own. Profiles you inherit — the ones you did not build — are read-only here. A 403 says so; copying one into your own organisation is a separate step.

  • The same rules a create is held to. Every reviewer must hold binding law, every source must already be in the library, and the limits on reviewers, sources and instruction length apply unchanged. See creating a profile for what each of those means. One thing an edit adds: a reviewer that already holds more sources than the limit allows keeps what it has and may not be given more, so the allowance only ever ratchets down.

  • A rule set is named by its snippetKey, never by its id. GET /v1/snippets publishes both fields on every record, and an analysis looks a rule set up by the key alone — so snippetIds takes keys, in spite of what the field is called. A document id there is a 400 naming each value you sent beside the key that record answers to. Worth reading twice on an edit: workers replaces what is stored rather than merging into it, so this is the write that decides which sources your reviewers hold from here on.

  • The server owns some fields. id comes from the path, status never moves on an edit, the legal orders follow the provisions your reviewers cite, and the authorship fields are ours. All four are refused rather than accepted and overwritten.

  • A jurisdiction has to be one we carry. SE, NO or EU, matched exactly — se is not SE. Anything else is a 400 naming what you sent.

  • A shortcut another of your profiles holds. urlSlug is unique within your organisation, and taking one already in use is a 409 naming the profile that has it.

  • A field that is not in the table above is refused, naming it. The same goes for a field on a reviewer. This matters more on an edit than anywhere else: a 200 means every field you sent replaced what was stored, so a misspelled field name is a 400 rather than an edit you believe you made.

  • We have to be able to weigh your sources at all. Deciding which of a reviewer's sources bind means reading the norm hierarchy of its legal orders, and when that read fails we refuse rather than guess. Nothing is wrong with your request; retry, and tell us if it repeats.

Nothing is written unless all of it passes.

Which check refused

Every refusal above answers with a stable name in error.details.refusal, and the values that refusal names in error.details.values:

{
  "error": {
    "code": "NOT_FOUND",
    "message": "No profile of that id exists in this workspace.",
    "details": {
      "refusal": "profile-not-found",
      "values": {}
    }
  }
}

Branch on details.refusal, never on message. The message is prose written for a person reading it once, and we reword it whenever we can say the same thing better; the name is the contract and does not move.

The table below is every refusal an edit can answer with, the ones it shares with a create included. Two of them only an edit reaches: profile-not-found and profile-owned-by-another-organisation.

details.refusalWhat it means
profile-has-no-reviewerThe profile would be stored with no reviewers at all, so there would be nothing for an analysis to run.
too-many-reviewersThe profile names more reviewers than the organisation's limit allows.
too-many-legal-sourcesOne reviewer holds more legal sources — cited provisions and rule sets counted together — than the organisation's limit allows.
instruction-too-longOne reviewer's instruction is longer than the character limit.
reviewer-holds-no-binding-lawOne reviewer holds no source that binds in the profile's legal orders, so it could not substantiate a finding.
norm-hierarchy-unavailableThe norm hierarchy could not be read, so which sources bind could not be determined. A fault on our side rather than a problem with the request, and worth one retry.
legal-source-not-in-libraryA reviewer cites a statute or names a rule set the library does not hold. The API refuses rather than fetching it on demand, which the web application does.
snippet-named-by-document-idA reviewer's `snippetIds` names a rule set by its document `id` rather than by its `snippetKey`. `GET /v1/snippets` publishes both fields side by side and only the key is looked up when an analysis runs, so such a reference used to be stored and the source dropped from every report without a word.
provision-not-in-statuteA `pinpoint` addresses no provision of the work it cites — the refusal #428 added and the reference contradicted for months.
provision-is-repealedA reviewer cites a provision that has been repealed. The register keeps such a section as a numbered placeholder carrying no text, so the citation would give the reviewer a legal source with nothing in it. Only provisions the payload *adds* are refused — one a stored reviewer already holds is left alone, so an old profile stays editable.
jurisdictions-cannot-be-derivedThe profile cites no provision to derive its legal orders from, sent no `jurisdictions`, and the organisation has no default.
shortcut-already-taken`urlSlug` is already held by another profile of the same organisation, which would make the `/p/{slug}` link open the wrong one.
record-fails-data-contractThe composed profile or reviewer does not match the stored data contract — a field of the right name carrying a value of the wrong shape.
profile-not-foundNo profile of that id exists.
profile-owned-by-another-organisationThe profile exists but belongs to another organisation — an inherited profile is read-only until it is copied.

Errors

ErrorWhen
400
BAD_REQUEST
A field the server owns was sent (id, jurisdictions, authorship), workers is not an array, unlisted is neither true nor false, status is neither DRAFT nor PUBLISHED, a jurisdiction named is not one this API carries, an outputLanguage named is not one this API carries, or a legal source named is not in the library. Not probed: every spelling of it needs a body, and NOT_FOUND below proves the same route with none.
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 profile belongs to another workspace — an inherited SYSTEM profile is read-only here and has to be copied first (#301). Not probed: it needs an id the verifying tenant can see but does not own.
404
NOT_FOUND
No profile with that id in this workspace.
409
CONFLICT
The edit leaves the profile with no determinable legal order, or the urlSlug sent is a shortcut another of the organisation's profiles already answers on. Not probed: the first needs a tenant configured without a default one, the second a shortcut already taken.
429
TOO_MANY_REQUESTS
The organisation has spent its hourly configuration-write floor. This is an abuse floor and not a plan limit; no analysis quota is consumed. Not probed: reaching it would mean sending sixty writes.
500
INTERNAL_ERROR
An unexpected server-side failure. Not probable from outside — reaching it means something is wrong.

No separate permission to ask for

Every credential of yours may edit the profiles your organisation owns. A 403 here means something else: the profile belongs to another workspace — an inherited one — and has to be copied into yours before it can be changed.

This costs no runs, but it is metered

Editing configuration never spends an analysis from your quota. It does tick the same hourly floor as creating one — a guard against a client in a loop, reported by the X-RateLimit-* headers on this route and by GET /v1/quotas. An integration that syncs its profiles on a schedule will never see it; a 429 here means something of yours is looping.