Status: gjeldende (25.10.2026).
API-et lar et annet system, for eksempel Relation OS, sende PDF-dokumenter til
signering, følge med på statusen og hente det signerte dokumentet. Alle svar
er JSON med camelCase, bortsett fra PDF-nedlastingene. Feil har formen
{ "error": "melding", "code": "maskinkode" }. Meldingen er på norsk og kan
vises brukeren, men kan endres; test på code, som er stabil. Koder følger
ellers statusen: bad_request, unauthorized, forbidden, not_found,
method_not_allowed, conflict, gone, payload_too_large,
validation_failed, rate_limited, internal, i tillegg til mer spesifikke
som agreement_required, already_signed, hash_mismatch, locked,
too_soon, folder_not_found, organization_suspended og verify_required. Også ukjente stier (404)
og feil metode (405) svarer med denne formen.
Grunnadresse: https://<domene>/api/v1 (lokalt http://localhost:8090/api/v1).
Maskinlesbar beskrivelse: OpenAPI 3.1 i docs/api/openapi.yaml, servert uten
nøkkel på GET /api/v1/openapi.yaml. En test sjekker at hver rute under
/api/v1 står i filen og omvendt.
Autentisering
En administrator lager en API-nøkkel i Relation Sign (Integrasjoner, API-nøkler,
eller POST /api/api-keys). Nøkkelen vises én gang; Relation Sign lagrer
bare SHA-256 av den. Nøkler i produksjon starter med rs_live_, i utvikling
med rs_test_.
export RS=https://sign.relationlabs.no/api/v1
export RS_KEY=rs_live_...
curl -H "Authorization: Bearer $RS_KEY" "$RS/envelopes"
- API-et krever en plan med API: Bedrift, Konsern, Relation OS og Intern har
det med, Løs vekt bare med API-tillegget. Uten gir en gyldig nøkkel
403med en melding om planen, nye nøkler og webhooker kan ikke lages (403), og webhooker sendes ikke (se «Planer, tilgang og forbruk» idocs/specs/2026-10-18-relation-sign-mvp-design.md). - Manglende, ukjent, utløpt eller tilbakekalt nøkkel gir
401. - En nøkkel har tilgangen full (standard) eller bare lese (
scope: "read"). En lesenøkkel kan bare gjøreGET, og tørrkjøringene for dokumentmaler som ikke endrer noe (validate,check,preview); alt annet gir403medcode: "read_only_key". Tilgangen settes når nøkkelen lages og kan ikke endres; lag en ny nøkkel i stedet. - En nøkkel kan ha en utløpsdato (
expiresAt, innen tre år). Etter den gir den401med meldingen «API-nøkkelen er utløpt». - Integrasjoner viser når og fra hvilken IP-adresse hver nøkkel sist ble brukt, og hvem som laget den.
- Nøkkelen gjelder én organisasjon. En signering i en annen organisasjon gir
404, aldri403. - Nøkkelen ser alle signeringer i organisasjonen.
- En personlig nøkkel (se under) ser bare det eieren ser.
- Grenser per nøkkel: 300 forespørsler i minuttet, og 60 opprettelser i
minuttet. Over grensen kommer
429.
| Kode | Betyr |
|---|---|
| 400 | ugyldig forespørsel (JSON, status-filter) |
| 401 | nøkkel mangler eller er ugyldig |
| 403 | planen til organisasjonen har ikke API (Løs vekt uten API-tillegget), nøkkelen kan bare lese (code: "read_only_key"), eller Relation Labs har suspendert organisasjonen (code: "organization_suspended": organisasjonen er helt blokkert, og alle kall med nøkkelen avvises, lesing som skriving, til suspensjonen er opphevet; webhook-leveranser blir stående i køen) |
| 404 | finnes ikke i din organisasjon |
| 409 | passer ikke med statusen (for eksempel sende noe som er sendt), eller samme Idempotency-Key pågår |
| 415 | feil Content-Type |
| 422 | valideringsfeil, meldingen sier hva |
| 429 | for mange forespørsler |
Personlige nøkler
En personlig nøkkel lager hver bruker selv under Min profil, for sin egen
AI-agent (MCP-serveren i docs/mcp.md). Den har samme form og de samme
grensene som en vanlig nøkkel, med disse forskjellene:
- Den er eieren: den ser og gjør bare det eieren kan i appen. En avsender
ser sine egne signeringer og de konfidensielle hen har innsyn i; alle andre
gir
404. En administrator ser det administratoren ser. - Rollekravene i appen gjelder: en avsender ser navnene på godkjente
signatarer, men ikke telefon, e-post eller fødselsdato, og
folderpåPOST /envelopesgjenoppretter ikke en arkivert mappe (409) og tar ikke over en mappe en annen har laget. - Den sender alltid som eieren.
senderEmailkan utelates eller være eierens adresse; en annen gir422medcode: "personal_key". - Den slutter å virke (
401) når eieren deaktiveres eller fjernes fra organisasjonen, og en endret rolle gjelder fra neste kall. - Den krever en plan med API, som andre nøkler.
- Revisjonssporet viser «
via personlig nøkkel « »». GET /mesvarer med nøkkelen og eieren (user: { id, name, email, role }).
Personlig nøkkel på appens egne ruter
En personlig nøkkel kan også brukes på en eksplisitt liste med appens egne
ruter under /api (ikke /api/v1), som en innlogget økt for eieren i
nøkkelens organisasjon. Det er dette MCP-serveren bruker for alt som ikke er
i /api/v1: kontakter, signatarsett, meldingsmaler, mapper, godkjente
signatarer, håndtering av signeringer, og for administratorer brukere,
organisasjonsinnstillinger, plan og forbruk. Listen står i
internal/httpapi/personal_bridge.go; rutenes egne rollekrav gjelder (403
for en avsender på en administratorrute, 404 for andres signeringer).
- Administrasjon (brukere, godkjente signatarer og innstillingene for dem,
standard påminnelser, plan, moduler, logo, SharePoint-standardmappe) krever
i tillegg tilgangen
adminpå nøkkelen (403 admin_key_requiredellers). Bare en administrator kan lage en slik nøkkel, i nettleseren. Det samme gjelder skriving av godkjente signatarer i/api/v1. - Ruter som gir rett til å signere (QR-koden for hurtigsignering) er stengt
for lesenøkler, selv om de er
GET. - Godkjenning av koblingsforespørsler fra Relation OS gjøres aldri med nøkkel.
- Alt som ikke står på listen gir
403medcode: "personal_key_not_allowed", også for en administrator: innlogging, nøkler, passkeys, webhooks, tilkoblinger, avtaler, bytte av organisasjon, og å koble til eller fra Microsoft-kontoer. - En API-nøkkel fra Integrasjoner gir
403medcode: "personal_key_required". - En lesenøkkel kan bare gjøre
GET(403 read_only_key), planregelen for API gjelder, og revisjonssporet viser nøkkelen. - Rutene er appens interne grensesnitt og kan endres uten varsel. For egne
integrasjoner: bruk
/api/v1.
Finne en person: GET /contacts?q=&limit=
Organisasjonens «Kontakter» (mest brukt først), deretter aktive kolleger som
ikke står der: { contacts: [{ name, email, phone, method, source }] }, der
source er contact eller colleague og method er metoden kontakten sist
signerte med (eller null). q søker i navn, e-post og telefon; limit er 1
til 100, standard 20. Brukes til å finne ut hvem «Daniel» er.
Signeringer
Opprette: POST /envelopes
Enten multipart/form-data med feltene file (PDF) og payload (JSON), eller
application/json med PDF-en som fileBase64. Maks 25 MB PDF.
| Felt | Type | |
|---|---|---|
title |
streng, påkrevd | maks 200 tegn |
message |
streng | til signatarene, maks 4000 tegn |
signingOrder |
parallel (standard) eller sequential |
fast rekkefølge varsler én og én |
expiresInDays |
1 til 180, standard 7 | regnes fra sending |
signers |
1 til 20 | { name, email, phone?, method, birthDate?, pays?, delivery?, returnUrl?, receipt? }, method er bankid, otp_email eller otp_sms (krever phone). birthDate (ÅÅÅÅ-MM-DD, valgfri, bare BankID): fødselsdatoen i BankID avgjør da hvem som kan signere. pays (valgfri, standard false): signataren er en forbruker som skal betale, se Signatar som betaler. delivery, returnUrl og receipt: se Innebygd signering. |
externalRef |
streng, maks 200 | din referanse, for eksempel relation-os:stalbygg:change_order:<id>; ikke unik |
readAccess |
link (standard), email_code, sms_code eller bankid |
lesetilgang: hva signataren må gjøre før dokumentet vises, se under. sms_code krever phone på alle signatarene |
allowComments |
true (standard) eller false |
om signatarene kan kommentere i dokumentet. Med false kan de bare signere eller avvise (med begrunnelse), og ingen kan kommentere, heller ikke dere. Kan endres mens signeringen er et utkast |
attachSignedPdf |
true eller false, standard organisasjonens innstilling |
legger den signerte PDF-en ved e-postene alle får når alle har signert, i tillegg til lenken. Bare med readAccess link eller email_code og ikke konfidensiell; true ellers gir 422 attach_not_allowed. Se Signert PDF som vedlegg. Kan endres mens signeringen er et utkast |
theme |
dark (standard) eller light |
utseendet på siden signatarene og kopimottakerne åpner. Alle får det samme; hver mottaker kan likevel bytte på sin egen enhet. Kan endres mens signeringen er et utkast |
confidential |
true eller false (standard) |
konfidensiell signering: synlig bare for nøkkelen som laget den, og i appen for avsenderen og personene i viewerEmails. Andre nøkler får 404, administratorer ser den bare som et tall. Kan ikke slås av igjen. Se Konfidensielle signeringer |
viewerEmails |
liste med e-post, maks 50 | bare med confidential: true: aktive brukere i organisasjonen som skal kunne åpne den i appen, i tillegg til avsenderen. En adresse som ikke er en aktiv bruker gir 422 |
reminders |
se under, standard organisasjonens innstilling | automatiske påminnelser til signatarer som ikke har signert |
send |
bool, standard false |
true sender med en gang; feiler sendingen, opprettes ingenting |
senderEmail |
streng | gjør denne brukeren til avsender hvis hen er en aktiv bruker i organisasjonen. Ellers, i rekkefølge: nøkkelens reserveavsender (satt under Integrasjoner), den som laget nøkkelen, organisasjonens eldste aktive administrator. Deaktiverte brukere hoppes over; finnes ingen aktiv bruker, gir kallet 422 |
fileBase64 |
streng | bare for JSON |
folderId |
uuid | legger signeringen i en av organisasjonens mapper (se Mapper); en ukjent mappe gir 404, en arkivert 409 |
folder |
{ externalRef, name } |
finner mappen med denne externalRef, eller lager den øverst med name første gang (se Mapper). Ikke sammen med folderId (422) |
Header Idempotency-Key (valgfri, maks 200 tegn): se under.
# multipart
curl -X POST "$RS/envelopes" -H "Authorization: Bearer $RS_KEY" \
-H "Idempotency-Key: em-12-rev3" \
-F file=@endringsmelding.pdf \
-F 'payload={"title":"Endringsmelding EM-12","signingOrder":"parallel","expiresInDays":14,
"externalRef":"relation-os:stalbygg:change_order:7f3c","senderEmail":"pl@straye.no","send":true,
"signers":[{"name":"Kari Kunde","email":"kari@kunde.no","method":"bankid"}]}'
# JSON
curl -X POST "$RS/envelopes" -H "Authorization: Bearer $RS_KEY" -H "Content-Type: application/json" \
-d "{\"title\":\"Kontrakt\",\"send\":false,\"signers\":[{\"name\":\"Ola\",\"email\":\"ola@ue.no\",\"method\":\"otp_email\"}],
\"fileBase64\":\"$(base64 < kontrakt.pdf | tr -d '\n')\"}"
Svar 201 med en Envelope:
{
"id": "1b7c...",
"title": "Endringsmelding EM-12",
"status": "sent",
"signingOrder": "parallel",
"createdAt": "2026-10-19T08:00:00Z",
"sentAt": "2026-10-19T08:00:00Z",
"completedAt": null,
"expiresAt": "2026-11-02T08:00:00Z",
"externalRef": "relation-os:stalbygg:change_order:7f3c",
"createdBy": { "id": "…", "name": "Per Prosjektleder" },
"message": "",
"originalSha256": "9f2a…",
"originalPages": 3,
"originalSize": 182044,
"sealedSha256": null,
"version": 1,
"versions": [{ "version": 1, "sha256": "9f2a…", "pages": 3, "size": 182044, "by": "", "createdAt": "…" }],
"signers": [{
"id": "…", "position": 1, "name": "Kari Kunde", "email": "kari@kunde.no", "phone": "",
"method": "bankid", "status": "notified", "notifiedAt": "…", "viewedAt": null, "signedAt": null,
"declinedAt": null, "declineReason": "", "signedName": "", "signedBirthDate": "", "nameMismatch": false,
"birthDate": "", "identityMatch": "",
"withdrawnAt": null, "withdrawReason": "", "invalidatedAt": null, "invalidatedReason": "",
"delivery": "email", "returnUrl": "", "receipt": true, "appName": ""
}],
"events": [{ "id": "…", "type": "sent", "signerName": "", "createdAt": "…", "ip": "…", "data": {} }],
"mySigningPath": null,
"readAccess": "link",
"reminders": { "mode": "standard", "days": [3, 7], "beforeDeadlineDays": 2 },
"archivedAt": null,
"archiveReason": null,
"folderId": "5d0e…",
"folder": { "id": "5d0e…", "name": "P42 Gressvik", "color": null, "kind": "project", "archived": false }
}
Statuser: signering draft, sent, completed, declined, expired,
cancelled, withdrawn. Signatar pending, notified, viewed, signed,
declined, withdrawn.
archivedAt er satt når signeringen ligger i arkivet, og archiveReason er
da manual eller auto. Arkivet er bare en visning hos avsenderen: ingenting
er slettet, status er uendret, dokumentene kan hentes som før og lenkene til
signatarer og kopimottakere virker. Ferdige signeringer arkiveres automatisk
30 dager etter at de ble avsluttet. Arkivering, henting fra arkivet og
permanent sletting gjøres i Relation Sign, ikke gjennom API-et.
Påminnelser
reminders styrer de automatiske påminnelsene. Uten feltet får signeringen
organisasjonens standard, som en administrator setter under Organisasjon
(fra start: dag 3 og 7, og 2 dager før fristen).
mode |
Påminnelser |
|---|---|
off |
ingen automatiske påminnelser |
standard |
dag 3 og 7 etter invitasjonen |
custom |
dagene i days: 1 til 5 dager, hver mellom 1 og 60 |
beforeDeadlineDays (0 til 14, standard 2) sender én påminnelse til så mange
dager før fristen; 0 slår den av. Dager regnes i norsk tid. Bare signatarer
som har fått lenken, ikke har signert og har tur (fast rekkefølge), får
påminnelser, og aldri to e-poster samme døgn. Avsenderen får uansett et varsel
dagen før fristen når noen ikke har signert.
"reminders": { "mode": "custom", "days": [2, 5, 10], "beforeDeadlineDays": 1 }
Kortformer: "reminders": "off", "reminders": "standard" eller en liste med
dager, "reminders": [2, 5] (custom). Svaret viser alltid hele objektet;
standard viser days: [3, 7].
Lesetilgang og bekreftelse
Lenken alene lar signataren lese dokumentet (readAccess: "link"). Med en
annen readAccess må signataren bekrefte at det er dem før tittel, dokument,
kommentarer og signert PDF vises; til da viser signeringssiden bare hvem den
er fra, fornavnet, fristen og hvor koden går:
readAccess |
Bekreftelse før lesing | Forbruk |
|---|---|---|
link |
ingen | ingenting |
email_code |
sekssifret kode på e-post | gratis, ingen rad i forbruket |
sms_code |
sekssifret kode på SMS | hver SMS teller, også koder som sendes på nytt |
bankid |
innlogging med eID: norsk eller svensk BankID, dansk MitID, finsk eID eller Vipps, det som er slått på (navnet sjekkes mot signataren) | hver innlogging teller som én eID-bruk (BankID, MitID, finsk eID eller Vipps), også når den blir avbrutt eller feiler; samme pris for alle eID-er, og den trekker fra eID-bruken som er med i planen |
Uansett readAccess krever det å avvise, kommentere (og svare) og trekke en
signatur at signataren har bekreftet at det er dem: en kode på e-post (gratis;
uten e-postadresse på SMS, der hver SMS teller). Bekreftelsen gjelder
i to timer i nettleseren den ble gjort i. Selve signeringen er uendret: BankID
eller engangskode i signeringsøyeblikket, bundet til dokumentet, også når
signataren logget inn med BankID for å lese.
Kopimottakere og signatarer som henter den signerte PDF-en, må bekrefte med en
kode på e-post når signeringen har vært fullført i mer enn 30 dager. For
kopimottakere gjelder det fra første dag når readAccess ikke er link.
Bekreftelsene står i hendelsesloggen og revisjonsloggen (access_code_sent,
access_code_failed, access_verified, access_rejected, aldri selve koden),
men sendes ikke som webhooker.
Signert PDF som vedlegg
Når alle har signert, får avsenderen, signatarene og kopimottakerne en e-post
med lenke til det forseglede dokumentet i Relation Sign. Med
attachSignedPdf: true ligger den signerte PDF-en også ved e-posten, så hver
mottaker har sin egen kopi.
Et vedlegg er like åpent som innboksen det havner i. Derfor går det bare der
innboksen uansett er nøkkelen: lesetilgang link eller email_code. Med
sms_code eller bankid har dere sagt at e-posten ikke er nok, og en
konfidensiell signering er bare for avsenderen og personene med innsyn; der
legges PDF-en aldri ved. Blir et utkast konfidensielt, eller får det en
strengere lesetilgang, slås vedlegget av.
Uten attachSignedPdf gjelder organisasjonens innstilling (av, med mindre en
administrator har slått den på i appen under «Organisasjon»). En PDF som er for
stor til e-post (over omtrent 2,4 MB), sendes bare som lenke, og e-posten sier
det. GET /envelopes/{id} viser valget som attachSignedPdf. I appen spør
«Send til signering» om valget der det er tillatt.
Signatar som betaler: pays
En signatar med "pays": true er en forbruker som skal betale dere etter
dokumentet, for eksempel en kjøpekontrakt, et tilbud eller en
håndverkerkontrakt. Knappen på signeringssiden sier da «Signer med plikt til å
betale» (angrerettloven § 16; på engelsk «Sign with obligation to pay», svensk
«Signera med betalningsskyldighet», dansk «Signér med
betalingsforpligtelse»), med en linje under om hvordan hen signerer.
Når alle har signert, legges den signerte PDF-en ved e-posten til signataren
som betaler, uansett hva attachSignedPdf er. Alle andre, det vil si de andre
signatarene, avsenderen og kopimottakerne, får den slik attachSignedPdf sier,
og pays endrer aldri attachSignedPdf. To unntak gir bare lenken også til
den som betaler, og e-posten sier det:
- Lesetilgangen tillater ikke vedlegg (
sms_codeellerbankid). Medlinkelleremail_codelegges den ved. - Den signerte PDF-en er for stor til e-post (over omtrent 2,4 MB). Den
inneholder dokumentet to ganger, som sider og som
original.pdf, i tillegg til signatursiden og seglet. Det betyr at en opplastet PDF over omtrent 1 MB (1 000 000 byte) blir for stor, for eksempel en skannet kontrakt. Den signerte avtalen ligger da bak lenken i Relation Sign så lenge signeringen er lagret. Appen varsler om dette på signataren før dere sender.
Det gjelder en PDF dere laster opp, like mye som et dokument laget fra en mal.
curl -X POST "$RS/envelopes" -H "Authorization: Bearer $RS_KEY" \
-F file=@kjopekontrakt.pdf \
-F 'payload={"title":"Kjøpekontrakt","send":false,
"signers":[{"name":"Kari Kunde","email":"kari@kunde.no","method":"bankid","pays":true}]}'
- Ikke i en konfidensiell signering (
422 pays_not_confidential), fordi den aldri sendes med vedlegg. Et utkast som er konfidensielt, kan ikke gjøres åpent igjen, så da må dere lage en ny signering. - Ikke for en signatar som signerer på stedet (
method: drawn,422 pays_not_drawn), fordi den knappen ikke sier at signaturen gir plikt til å betale. - Ikke for en signatar som ikke får e-posten når alle har signert, det vil si
delivery: nonemedreceipt: false(422 pays_no_receipt). Medreceipt: true, eller utendelivery: none, går det. - I et utkast laget fra en mal bestemmer malen for sine egne parter (de med
rolle fra malen). De betaler som rollen sier, uansett hva forespørselen sier,
og
PATCHgir422 pays_template_role. En signatar som er lagt til for hånd, betaler nårpayssier det. Bytter dere ut malens dokument med en annen PDF, betaler ikke malens roller lenger. - En kopi («Lag kopi» i appen,
POST /envelopes/{id}/duplicate) betaler som originalen. En kopi av en signering laget fra en mal er ikke lenger malens: den er en vanlig PDF-signering, så dere kan endre hvem som betaler på kopien. - Et signatarsett lagrer aldri
pays. GET /envelopes/{id}viser det somsigners[].pays.- Dokumentet er deres. Det er dere som sørger for at det sier hva signataren skal betale, og at det er riktig signatar som er merket.
Konfidensielle signeringer
Med confidential: true ser bare nøkkelen som laget signeringen den. En annen
nøkkel i organisasjonen får 404 og finner den ikke i listen, og nøkkelen ser
aldri en konfidensiell signering som er laget i appen. I appen kan avsenderen
(senderEmail) og personene i viewerEmails åpne den, men ikke
administratorene. Innsyn kan endres i appen etterpå. Det kan ikke gjøres om:
confidential: false på et konfidensielt utkast gir 422. Webhooker sier hva som
skjedde, ikke hva det gjelder: data.envelope har bare id, externalRef,
status, signingOrder, version, tidspunktene og "confidential": true.
data.signer og data.comment mangler. Hendelsene confidential_set,
viewer_added og viewer_removed står i hendelsesloggen, men sendes ikke som
webhooker.
Hente: GET /envelopes/{id}
curl -H "Authorization: Bearer $RS_KEY" "$RS/envelopes/$ID"
Svar 200 med Envelope.
Liste: GET /envelopes?externalRef=&status=&archived=&folderId=&subfolders=
curl -G -H "Authorization: Bearer $RS_KEY" "$RS/envelopes" \
--data-urlencode "externalRef=relation-os:stalbygg:change_order:7f3c"
Svar { "envelopes": EnvelopeSummary[] }, nyeste først, maks 500.
Arkiverte signeringer er ikke med som standard: archived=true gir bare de
arkiverte, archived=all begge, archived=false (standard) de som ikke er
arkivert; andre verdier gir 400. Slår dere opp på externalRef for å finne
status, bruk archived=all, ellers forsvinner en signering fra svaret 30 dager
etter at den ble avsluttet. GET /envelopes/{id} svarer alltid, arkivert eller
ikke.
folderId er en mappe-id (bare signeringer rett i mappen, med
subfolders=true også i mappene under), none (uten mappe) eller archived
(i en arkivert mappe); en ukjent mappe gir 404.
EnvelopeSummary er Envelope uten message, hashene, events og
versions, og med signers: [{ name, status }].
Endre et utkast: PATCH /envelopes/{id}
Tittelen og innstillingene til et utkast, for eksempel et utkast fra en mal, før det sendes, og hvem av signatarene som betaler. Bare feltene som er med, endres:
curl -X PATCH "$RS/envelopes/$ID" -H "Authorization: Bearer $RS_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Samboeravtale Hauk og Martine", "message": "Les gjennom før du signerer.",
"expiresInDays": 14, "signingOrder": "sequential", "reminders": [3, 7],
"confidential": true, "viewerEmails": ["anne@straye.no"], "folderId": null}'
curl -X PATCH "$RS/envelopes/$ID" -H "Authorization: Bearer $RS_KEY" \
-H "Content-Type: application/json" \
-d '{"signers": [{"id": "'$SIGNER_ID'", "pays": true}]}'
| Felt | |
|---|---|
title, message, expiresInDays, signingOrder, reminders |
som på POST /envelopes |
confidential |
true gjør utkastet konfidensielt; false på et konfidensielt gir 422 (det kan ikke gjøres åpent igjen) |
viewerEmails |
erstatter hvem som har innsyn i tillegg til avsenderen; [] gir bare avsenderen; på et åpent utkast 422 |
folderId |
mappen utkastet skal ligge i; null tar det ut av mappen |
signers |
[{ id, pays }]: om signataren med denne id-en (fra signers[].id) betaler, se Signatar som betaler. Hver signatar høyst én gang, og pays må være med. En id utkastet ikke har, også id-en til en signatar i en annen signering, gir 422. Alle signatarene beholder id-ene sine, også ved siden av en som signerer på stedet |
Svar 200 med Envelope. Reglene er de samme som når utkastet endres i
appen: et utkast nøkkelen ikke ser, også i en annen organisasjon, gir 404
(en personlig nøkkel ser det eieren ser: en avsender sine egne og dem hen har
innsyn i), en signering som ikke lenger er et utkast gir 409, og utkastet slik
det blir, sjekkes som en ny signering (422). Dokumentet, kopimottakerne,
lesetilgangen og signatarene, utenom hvem som betaler, endres ikke her, og et
felt ruten ikke kjenner, eller noe etter JSON-objektet, gir 400 i stedet for
å bli oversett. En lesenøkkel får 403 read_only_key.
- En nøkkel fra Integrasjoner eller en tilkobling ser bare de konfidensielle
signeringene den har laget selv, så den kan bare gjøre egne utkast
konfidensielle (som med
POST /envelopes); et annet gir422 confidential_not_own, og ingenting endres. - Er endringen lagret, men nøkkelen ser ikke lenger utkastet (en med innsyn tok
seg selv av listen), er svaret
200med{ id, status: "draft", confidential: true, visible: false, message }, ikke404. - Den som betaler, beholder det til
signerssier noe annet, og får den signerte PDF-en som vedlegg i sin egen e-post.attachSignedPdfendres ikke av hvem som betaler. Malens egne parter betaler som malen sier (422 pays_template_rolehvis dere prøver å endre det). - Et utkast der noen betaler, kan ikke gjøres konfidensielt (
422 pays_not_confidential), fordi en konfidensiell signering aldri sendes med vedlegg. Det gjelder også vedfill, og vedfillmedenvelopeIdpå et utkast som er konfidensielt fra før, når malen nå har en rolle som betaler. pays: truegir422 pays_no_receiptpå en signatar meddelivery: noneogreceipt: false.expiresInDaysmå være 1–180 her og påfill;0gir422. (PåPOST /envelopesbetyr0standardfristen, 7 dager.)- Ruten tar ikke
Idempotency-Key: samme endring to ganger gir samme utkast. Høyst 60 i minuttet per nøkkel.
Sende: POST /envelopes/{id}/send
Bare utkast (draft), ellers 409.
curl -X POST -H "Authorization: Bearer $RS_KEY" "$RS/envelopes/$ID/send"
Avbryte: POST /envelopes/{id}/cancel
Bare sent, og ikke når alle har signert (forsegling pågår).
curl -X POST -H "Authorization: Bearer $RS_KEY" "$RS/envelopes/$ID/cancel"
Purre: POST /envelopes/{id}/remind
Sender invitasjonen på nytt til alle som har tur og ikke har signert. Den som
har fått e-post de siste ti minuttene, hoppes over. Bare sent, ellers 409.
curl -X POST -H "Authorization: Bearer $RS_KEY" "$RS/envelopes/$ID/remind"
{ "reminded": 2, "throttled": ["Ola Hansen"], "envelope": { "id": "…", "…": "…" } }
Purre én signatar: POST /envelopes/{id}/signers/{signerId}/remind
Sender invitasjonen på nytt til én signatar som har tur. Har signataren fått
e-post de siste ti minuttene, gir det 429 med code: "too_soon". Svarer med
signeringen. signerId er signers[].id. En signatar med delivery: "none"
får ikke e-post fra oss og gir 409 med code: "delivery_none"; «Purr alle»
hopper over dem.
curl -X POST -H "Authorization: Bearer $RS_KEY" "$RS/envelopes/$ID/signers/$SIGNER/remind"
Hente signeringslenken: POST /envelopes/{id}/signers/{signerId}/link
Gir lenken signataren fikk på e-post, så en integrasjon kan vise den til
signataren selv, for eksempel en AI-assistent som sender en kontrakt til
brukeren den snakker med og gir lenken i chatten. Bare når signataren har tur
og ikke har signert, ellers 409; en signatar med method: drawn bruker
QR-koden. Krever en nøkkel med full tilgang.
Lenken åpner bare signeringssiden. Selve signeringen krever fortsatt
signatarens egen BankID (navnet sjekkes) eller en kode til signatarens egen
e-post eller telefon, så den som henter lenken kan ikke signere for noen
andre. readAccess gjelder som før. Hver henting står i hendelsesloggen
(link_issued) med hvilken nøkkel som hentet den. Gi lenken bare til
signataren den gjelder.
curl -X POST -H "Authorization: Bearer $RS_KEY" "$RS/envelopes/$ID/signers/$SIGNER/link"
{ "url": "https://sign.relationlabs.no/s/…", "signerId": "…", "signerName": "Kari Kunde", "signerStatus": "notified", "method": "bankid" }
BankID kan ikke startes fra API-et: signataren åpner lenken og trykker «Signer med BankID». Si derfor «åpne lenken og signer med BankID», ikke «bekreft BankID på telefonen».
Rette en signatar: PATCH /envelopes/{id}/signers/{signerId}
Retter name, email, phone, method, locale eller birthDate for en
signatar som ikke har signert ennå. Bare feltene som er med, endres. Ny
e-postadresse gir en ny lenke, og den gamle slutter å virke. En signatar som
har signert eller avvist, gir 409. delivery: "email" flytter en signatar
fra innebygd signering til e-post (se under).
curl -X PATCH -H "Authorization: Bearer $RS_KEY" -H "Content-Type: application/json" \
-d '{"email":"kari.nordmann@kunde.no"}' "$RS/envelopes/$ID/signers/$SIGNER"
Arkivere: POST /envelopes/{id}/archive og /restore
Arkivering flytter signeringen ut av lista, men sletter ingenting; hent den
med archived=only eller archived=all. En signering som venter på
signaturer, må avbrytes først (422). /restore legger den tilbake.
Kommentarer: GET /envelopes/{id}/comments
Kommentarene på signeringen, eldste først, fra alle versjoner. page, x og
y sier hvor i dokumentet en kommentar er festet (x og y som andel av
siden), og parentId hvilken kommentar den svarer på. En kommentar som
markerer tekst i dokumentet, har også quote (teksten som ble markert) og
highlight (de markerte linjene som [x, y, bredde, høyde], andeler av
siden); nålen (x, y) står da i margen ved siden av utdraget. Virker også
med en nøkkel som bare kan lese.
{ "comments": [{ "id": "…", "version": 1, "parentId": null, "authorKind": "signer",
"authorName": "Kari Nordmann", "body": "Feil beløp i punkt 3", "page": 2, "x": 0.41, "y": 0.63,
"createdAt": "2026-10-20T09:12:00Z" }] }
Dokumenter
| Kall | Svar |
|---|---|
GET /envelopes/{id}/document |
originalen (PDF) |
GET /envelopes/{id}/signed |
forseglet PDF med signaturside, original og evidence.json som vedlegg; bare completed, ellers 409 |
GET /envelopes/{id}/evidence |
evidence.json slik den ligger i den forseglede PDF-en; bare completed |
GET /envelopes/{id}/evidence.zip |
bevispakken: signert.pdf, originalen av hver versjon (originaler/versjon-N.pdf), audit.pdf, evidence.json og README.txt om hvordan dokumentet kontrolleres; bare completed, ellers 409 |
curl -H "Authorization: Bearer $RS_KEY" -o signert.pdf "$RS/envelopes/$ID/signed"
curl -H "Authorization: Bearer $RS_KEY" "$RS/envelopes/$ID/evidence"
curl -H "Authorization: Bearer $RS_KEY" -o bevispakke.zip "$RS/envelopes/$ID/evidence.zip"
evidence.json i bevispakken har format: "relation-sign/evidence-package/v1"
og lages når pakken hentes: hendelseskjeden med prevHash og hash for hver
hendelse (oppskriften står i chain.algorithm), signatarene og hvordan de
signerte, seglet, SHA-256 for alle dokumentversjoner og for hver fil i pakken.
Fødselsdato, rå identitetsdata fra BankID og engangskoder er aldri med.
evidence.json har format: "relation-sign/evidence/v1", signatarene med
identitet og tidspunkt, hele hendelseskjeden med hasher, versjoner og
kommentarer. Fødselsnummer er aldri med.
Innebygd signering
For et hvilket som helst system som vil ha signering i sin egen flyt, for eksempel regnskap, kontrakter, eiendomsmegling eller CRM: brukeren trykker «Signer» i programmet, signerer i Relation Sign med BankID eller kode, og kommer rett tilbake til programmet.
Brukeren trykker «Signer» → POST /envelopes { send: true, signers: [{ delivery: "none", returnUrl }] }
← 201 { signers: [{ signingUrl }] }
Programmet sender brukeren til signingUrl → BankID → signert
Relation Sign sender brukeren til returnUrl?rs_envelope=…&rs_signer=…&rs_status=signed
Webhook signer.signed, så envelope.completed → programmet henter /signed
Felt på signataren
| Felt | ||
|---|---|---|
delivery |
email (standard) eller none |
none: vi sender ingen invitasjon, påminnelse eller annen e-post om signeringen. Engangskoder (otp_email, otp_sms, lesetilgang med kode) sendes fortsatt: de er identitetsbekreftelsen. Ikke for method: drawn |
returnUrl |
streng, maks 2000 | hvor signeringssiden sender signataren etter å ha signert, avvist eller trykket «Tilbake». Må ligge på en av integrasjonens tillatte adresser (se under), ellers 422 med code: "return_url_not_allowed". Virker også med delivery: "email" |
receipt |
bool, standard true |
bare delivery: "none": om signataren får kvitteringen med det signerte dokumentet på e-post når alle har signert |
appName |
streng, maks 60 | navnet signataren ser på vei tilbake («Fortsett i …»). Bare når returnUrl ligger på en av Relation Labs sine egne adresser (se under); ellers brukes navnet på API-nøkkelen, og feltet gir 422 |
Signeringen vises fortsatt i Relation Sign. Avsenderen ser «Signerer i Eksempel Regnskap» på signataren, og kan trykke «Send på e-post i stedet». Feltene kan bare settes fra API-et.
Lenken: signingUrl
Svaret fra POST /envelopes med send: true og fra
POST /envelopes/{id}/send har signingUrl på hver signatar med
delivery: "none" som har tur. Det er hele lenken; send brukeren dit, i samme
fane eller en ny. Hver utlevering står i hendelsesloggen (link_issued).
GET, lister og webhooks viser aldri lenken.
Med signingOrder: "sequential" har senere signatarer ikke tur ennå. Når turen
kommer, sender vi webhooken signer.ready; hent da lenken med
POST /envelopes/{id}/signers/{signerId}/link. Det kallet tar også en valgfri
{ "returnUrl": "…" } som erstatter den lagrede, så du kan sende brukeren
tilbake til akkurat den siden hen kom fra.
Tilbake til programmet
Etter signeringen viser siden seglet og sender brukeren videre etter tre sekunder, med knappen «Fortsett i Eksempel Regnskap» for den som ikke vil vente. Vi venter ikke på forseglingen. Ved avvisning får brukeren knappen. Lenken «Tilbake til Eksempel Regnskap» øverst på siden lar brukeren gå tilbake uten å signere.
https://app.eksempel.no/dokumenter/123?rs_envelope=<id>&rs_signer=<id>&rs_status=signed
rs_status |
|
|---|---|
signed |
brukeren signerte |
declined |
brukeren avviste |
aborted |
brukeren gikk tilbake uten å signere; ingenting er endret |
Hvem som helst kan skrive en slik adresse. Stol derfor på webhooken eller
GET /envelopes/{id}, aldri på rs_status alene.
Tillatte adresser
En returnUrl må ligge på en adresse integrasjonen har fått godkjent, så
sign.relationlabs.no aldri kan brukes til å videresende noen til en
falsk side etter en ekte BankID-innlogging.
- API-nøkkel: en administrator legger inn adressene under Integrasjoner → API-nøkkelen → Signering i eget system, sammen med appnavnet brukeren ser på knappen.
- Tilkobling fra katalogen (OAuth): klientens
redirectUrisgir de tillatte adressene, og appnavnet er katalognavnet. Kunden trenger ikke gjøre noe. - Relation Labs sine egne adresser, som kundeportalen i Relation OS, er
tillatt for alle nøkler og tilkoblinger (
TRUSTED_RETURN_ORIGINSpå serveren). Der kan hver signatar også få sitt egetappName.
En tillatt adresse er en opprinnelse, som https://app.eksempel.no, med
eventuell port. https://*.eksempel.no dekker alle underdomener (ikke
eksempel.no selv). returnUrl må være en hel https-adresse på nøyaktig
samme opprinnelse. http://localhost og http://127.0.0.1 er tillatt for
lokal utvikling.
Når brukeren aldri signerer i programmet
PATCH /envelopes/{id}/signers/{signerId} med { "delivery": "email" } gir
signataren invitasjonen på e-post med en gang (hvis hen har tur), og
påminnelsene som vanlig fra da av. Motsatt vei går ikke (422).
Lenke til «Ny signering»
Uten API og uten nøkkel: en lenke eller knapp i et hvilket som helst system som åpner «Ny signering» ferdig utfylt. Den som klikker, må være logget inn i Relation Sign (ellers logger hen inn først og kommer tilbake til lenken), velger PDF-en og sender som vanlig. Alt i lenken er forslag som kan endres i skjemaet.
https://sign.relationlabs.no/ny?tittel=Avtale+1042&ref=ORD-1042&frist=14
&navn=Kari+Kunde&epost=kari@kunde.no&mobil=
&navn=Ola+Nordmann&epost=ola@kunde.no&mobil=%2B4791234567
| Parameter | Hva |
|---|---|
tittel |
Tittelen på signeringen (høyst 200 tegn) |
melding |
Meldingen til signatarene (høyst 2000 tegn) |
navn, epost, mobil |
Én signatar per gang de gjentas, i samme rekkefølge. mobil kan være tom. Høyst 20 |
frist |
Fristen i dager, 1–180 (ellers organisasjonens vanlige) |
ref |
Systemets egen referanse (høyst 200 tegn). Lagres som externalRef, kommer tilbake i webhookene og kan brukes i GET /envelopes?externalRef= |
Lenken lagrer seg som det påbegynte skjemaet med en gang, så en omlasting finner det igjen. En lenke kan settes sammen under «Integrasjoner» → «Katalog» → «Lenke fra eget system».
Mapper
Mapper ordner signeringene per kunde, prosjekt eller noe annet. De hører til
organisasjonen, kan ligge inntil tre nivåer dypt, og et navn er unikt blant
mappene på samme sted (uansett store og små bokstaver). En signering ligger i
én mappe eller ingen. externalRef er deres egen id for mappen, for eksempel
et prosjekt i Relation OS, og er unik i organisasjonen.
Liste: GET /folders?archived=
curl -H "Authorization: Bearer $RS_KEY" "$RS/folders"
Svar:
{
"folders": [{
"id": "5d0e…", "parentId": null, "name": "P42 Gressvik", "color": null, "kind": "project",
"externalRef": "relation-os:stalbygg:project:42", "createdAt": "…", "archivedAt": null,
"depth": 1, "path": ["P42 Gressvik"], "count": 3, "total": 5
}],
"all": 12, "unfiled": 4, "inArchived": 0, "maxDepth": 3
}
Mappene kommer dybde først, sortert på navn. count er signeringene rett i
mappen og total med mappene under; arkiverte signeringer telles ikke.
Arkiverte mapper er bare med når archived=true.
Opprette: POST /folders
| Felt | Type | |
|---|---|---|
name |
streng, påkrevd | maks 80 tegn |
parentId |
uuid | mappen den skal ligge i; maks tre nivåer |
externalRef |
streng, maks 200 | deres id for mappen |
color |
slate, blue, teal, green, amber, orange, red eller violet |
valgfri |
kind |
customer eller project |
valgfri |
Svar 201 med mappen. Finnes det allerede en mappe med samme externalRef,
svarer kallet 200 med den, så det er trygt å prøve igjen. Et navn som er
tatt på samme sted gir 409.
Mappe per prosjekt fra Relation OS
Send folder når signeringen opprettes:
{ "title": "Endringsmelding EM-12", "folder": { "externalRef": "relation-os:stalbygg:project:42", "name": "P42 Gressvik" }, "signers": [ … ] }
Første gang lages mappen øverst med name (og kind: "project"); senere
kall med samme externalRef bruker den samme mappen, også om name er
endret. En mappe som er arkivert, hentes tilbake. Finnes det allerede en mappe
øverst med samme navn og uten externalRef, tas den i bruk. Er navnet tatt av
en annen mappe, får den nye mappen externalRef i parentes bak navnet.
Å flytte signeringer mellom mapper, gi nytt navn, arkivere og slette gjøres i
Relation Sign. En arkivert mappe er skjult, men signeringene i den beholder
folderId; bare en tom mappe kan slettes.
Dokumentmaler
Malene i «Dokumentmaler» kan fylles ut med API-et, og en integrasjon eller en AI-agent kan skrive egne maler i Markdown. Serveren er fasit: den har de samme reglene som veiviseren i appen (påkrevd, påkrevd når en del vises, skjult, summer, partsfelt, enheter, utregnede kolonner), og sier hva som mangler med etikett og kode, så en agent kan stille oppfølgingsspørsmålene uten egne regler.
- En mal i en annen organisasjon gir
404. Alle i organisasjonen ser og fyller ut organisasjonens maler; den som laget en mal, og en administrator, kan endre, publisere og arkivere den (403for andre, også med en personlig nøkkel). - En lesenøkkel kan lese, sjekke svar (
check), forhåndsvise (preview) og sjekke en mal (validate), men ikke lage, lagre, fylle ut, publisere eller arkivere (403 read_only_key). Det samme gjelder en tilkobling med bareenvelopes:read. - Utfylling (
fill) krever det samme somPOST /envelopes(avtalen), og lager bare et utkast. Det sendes medPOST /envelopes/{id}/send, som før. - «Dette er meg» (
me: truepå en part) er eieren av en personlig nøkkel. Med en nøkkel fra Integrasjoner er det avsenderen:senderEmail, ellers nøkkelens reserveavsender, som påPOST /envelopes. En personlig nøkkel med en annensenderEmailgir422 personal_key. - Grenser per nøkkel: utfylling 20 i minuttet, ny mal og lagring 60, publisering
30, sjekk og
validate120, og 120 forhåndsvisninger i minuttet som ikke ligger i bufferen (429medretryAfter).
Hvilken mal: ref
ref |
Er |
|---|---|
standard:samboer |
en standardmal (slug) |
7c1e… |
en egen mal, versjonen som gjelder |
7c1e…@3 |
versjon 3 av en egen mal |
7c1e…@draft |
utkastet til en egen mal: kan leses, sjekkes og forhåndsvises, men ikke fylles ut |
Liste: GET /document-templates?q=
Organisasjonens publiserte egne maler (etter navn), så standardmalene for
kontotypen i samme rekkefølge som i appen (en bedrift får kategoriene
Avtaler, Arbeid og oppdrag og Selskap først, med den mest brukte malen
først i hver). q søker som biblioteket i appen: hvert ord i q må stå i
navnet eller beskrivelsen til en egen mal, eller i navnet, sammendraget,
loven, kategorien eller søkeordene (keywords) til en standardmal, uansett
store og små bokstaver. Småord som «av», «og» og «for», og ord rundt et
malsøk som «mal», «signering» og «pdf», telles ikke med når q har andre
ord, så både q=mc og q=salg av bil finner «Salg av kjøretøy».
Standardmalene som treffer best, kommer først: et ord som er et helt ord i
navnet eller et søkeord, teller mer enn starten av et ord, som teller mer
enn et treff inni et ord, og alle foran dem som har ordene bare i
sammendraget, loven eller kategorien.
{ "templates": [
{ "ref": "7c1e…", "kind": "own", "name": "Leie av parkeringsplass", "summary": "…", "version": "2", "pinnedRef": "7c1e…@2" },
{ "ref": "standard:samboer", "kind": "standard", "name": "Samboeravtale", "summary": "…",
"category": "samliv", "categoryLabel": "Samliv", "audience": "private",
"keywords": ["samboer", "samboerkontrakt", "…"], "version": "2026-11-03.3", "pinnedRef": "standard:samboer" }
] }
Spørsmålene: GET /document-templates/{ref}
Partene (roles), spørsmålene i veiviserens rekkefølge (questions, gruppe for
gruppe), summene (sums), titlePattern og «Før du begynner» (warnings) for
en standardmal. Hvert spørsmål sier når det spørres (shownWhen) og når det må
besvares (requiredWhen), i ord og som vilkår (anyOf: ett av dem holder), og
hvordan svaret skrives (answer). null betyr alltid (spørres) og aldri (ut
over required).
{
"ref": "standard:samboer", "kind": "standard", "name": "Samboeravtale", "version": "2026-11-03.3",
"warnings": ["Samboere arver ikke hverandre etter loven, …"],
"roles": [{ "key": "samboer", "label": "Samboer", "signs": true, "min": 2, "max": 2,
"fields": ["name", "email", "phone", "address", "birthDate"], "required": ["name", "email"] }],
"questions": [
{ "key": "bolig", "label": "Boligen", "type": "choice", "group": "Boligen", "required": true,
"requiredWhen": null, "shownWhen": null, "answer": "ett av alternativene i options, skrevet nøyaktig likt",
"options": ["Vi eier boligen sammen", "Den ene av oss eier boligen", "Vi leier boligen"] },
{ "key": "bolig_eier", "label": "Hvem eier boligen", "type": "party", "group": "Boligen", "required": false,
"requiredWhen": { "text": "når «Boligen» er «Den ene av oss eier boligen»",
"anyOf": [{ "key": "bolig", "is": "Den ene av oss eier boligen" }] },
"shownWhen": { "text": "når «Boligen» er «Den ene av oss eier boligen»",
"anyOf": [{ "key": "bolig", "is": "Den ene av oss eier boligen" }] },
"answer": "en av partene, {\"role\": \"<rolle>\", \"index\": 0} …", "parties": { "roles": ["samboer"], "others": [] } }
],
"sums": [{ "keys": ["andel_1", "andel_2"], "equals": 100, "text": "«Samboer 1 sin eierbrøk» og «Samboer 2 sin eierbrøk» skal bli 100 til sammen" }],
"titlePattern": ""
}
Et vilkår er { "key", "is" } (et alternativ, eller true for ja), { "key", "isNot" } (ikke dette alternativet, eller ikke ja, også ubesvart) eller { "key", "filled": true|false }. min og max står i svarets form: øre for et
beløp, ÅÅÅÅ-MM-DD for en dato.
En rolle har help når malen sier noe om parten. En rolle med "min": 0 er
en part som kan utelates, også en som signerer: den trengs når en del som
vises, skriver den (neededWhen), og står den bare i valgfrie deler, spørres
den bare når en av dem vises (shownWhen, med samme vilkår). Ellers legger
du den til eller ikke. Er den ikke med, sender du ingen parter for den (eller
en tom liste); den blir verken signatar eller kort, og parter sendt for en
rolle som ikke spørres, fjernes. I gavebrevet er det slik for Mottaker og
Verge:
{ "key": "verge", "label": "Verge", "help": "Vergen eller vergene tar imot gaven og signerer for barnet, …",
"signs": true, "min": 0, "max": 2, "fields": ["name", "email", "phone", "address"], "required": ["name", "email"],
"neededWhen": { "text": "når «Mottaker er under 18 år» er ja", "anyOf": [{ "key": "mindrearig", "is": true }] },
"shownWhen": { "text": "når «Mottaker er under 18 år» er ja", "anyOf": [{ "key": "mindrearig", "is": true }] } }
Spørsmålene som avgjør om en slik part er med (mindrearig her), står først
i questions, med group «Partene», slik veiviseren stiller dem rett over
partens kort. Et partsfelt kan bare peke på en part som er med: peker det på
en part som fjernes, gir utfyllingen 422 party_unknown.
Svarene sendes som values: { fields, parties }:
| Type | Svaret i fields |
|---|---|
text, longtext, address |
tekst (høyst 500, 5000 og 300 tegn) |
number, percent |
tall (prosent 0–100, høyst to desimaler) |
money |
øre som heltall: 1250000 er kr 12 500,00 |
date |
"2026-11-03" |
choice |
ett av options, nøyaktig likt |
boolean |
true eller false |
orgnr, regnr |
"923609016", "AB 12345" |
party |
{ "role": "samboer", "index": 1 } (fra 0) eller { "other": "Begge" } |
rows |
[{ "kolonne": verdi, … }]; utregnede kolonner sendes ikke |
parties er partene per rolle i rekkefølge: { "samboer": [{ "me": true }, { "name": "Martine Berg", "email": "martine@example.no" }] }. Hver part i en
rolle som signerer, trenger e-post (invitasjonen går dit); BankID blir metoden.
pinnedRef er akkurat versjonen spørsmålene gjelder (<id>@3; for en
standardmal det samme som ref). Fyll ut den, så kommer ikke en ny versjon
imellom. Et utkast (<id>@draft) har ingen pinnedRef: det endres ved hver
lagring, og det fylles aldri ut.
Med ?format=markdown kommer malen som Markdown i stedet (text/markdown),
en standardmal slik en egen mal ville hatt den, med eksempelet. Med
?format=markdown-json kommer det samme som JSON: { ref, markdown, notes },
der notes er det Markdown ikke kan si (code: not_in_markdown, med path).
(format=markdown med Accept: application/json gir også JSON.) Det er
utgangspunktet for «lag en mal basert på samboeravtalen, men …»: endre teksten
og send den til POST /document-templates. Se Egne maler i
Markdown.
Sjekke svarene: POST /document-templates/{ref}/check
{ values, senderEmail? } → hva en utfylling med disse svarene ville avvist,
uten PDF:
{
"ok": false,
"missing": [
{ "key": "samboer.1.email", "label": "Samboer 2: e-post", "help": "Invitasjonen til å signere sendes hit.", "code": "signer_email" },
{ "key": "bolig_eier", "label": "Hvem eier boligen", "code": "required", "why": "når «Boligen» er «Den ene av oss eier boligen»" },
{ "key": "utgifter", "label": "Hvordan deler dere de felles utgiftene?", "code": "required" }
],
"fieldErrors": {},
"hidden": ["kjopesum", "egenkapital_1", "…"]
}
missing står i veiviserens rekkefølge (partene først), med code
required, signer_email, too_few_parties (roles.<rolle>, med params.min,
og why for en part som kan utelates og som en del som vises, trenger)
eller no_signer. fieldErrors er svar som er feil (samme koder som en 422 fra
fill, se fieldErrors under). hidden er spørsmålene svarene skjuler: de
skal ikke spørres, og svarene på dem fjernes. roles.<rolle> i hidden er en
part som kan utelates og ikke skal spørres; partene for den fjernes. ok er true når fill går
gjennom.
Forhåndsvise: POST /document-templates/{ref}/preview
{ values, sample?, letterhead? } → PDF-en med svarene så langt; det som
mangler står som «[Etikett]», og sample: true fyller inn malens eksempel der
svar mangler. letterhead: false tar organisasjonens brevhode av en
standardmal (se Brevhodet).
Fylle ut: POST /document-templates/{ref}/fill
{ values, title?, envelopeId?, senderEmail?, letterhead?, …innstillinger } → et utkast til signering med
PDF-en og partene som signerer som signatarer, svart som GET /envelopes/{id} (201). Uten title gir malens titlePattern navnet. Med
envelopeId fylles et utkast fra samme mal på nytt («Endre svarene», 200,
samme id): bare ens eget utkast, ellers 409 (draft_not_own,
draft_other_template, not_from_template), og 404 for et man ikke ser;
uten letterhead beholder utkastet brevhodevalget det hadde.
Svar som ikke går gjennom gir 422 validation_failed med fields og
fieldErrors ({ "utgifter": { "code": "required", "message": "må fylles ut" } }).
Idempotency-Key virker som på POST /envelopes. Utkastet sendes med POST /envelopes/{id}/send.
Utkastets innstillinger kan gis med det samme, med navnene fra POST /envelopes: message, expiresInDays, signingOrder, reminders,
confidential, viewerEmails og folderId (null: ingen mappe). De sjekkes
sammen med utkastet: en innstilling som ikke går, gir 422 (en mappe nøkkelen
ikke ser 404), og da lages ikke noe utkast. Fylles et utkast ut på nytt
(envelopeId), endres bare innstillingene som er med. Etterpå endres de med
PATCH /envelopes/{id}.
Brevhodet: letterhead
En bedrift kan ha ett brevhode for dokumentmaler (Organisasjon › «Merkevare»,
«Logo eller banner på standardmalene»): logoen sin, eller et banner, øverst på første side av
standardmalene, mens «Laget med Relation Sign» står nederst som før. Når det er
slått på for standardmaler, får en standardmal det fra preview og fill uten
at noe sendes. letterhead: false gir ett dokument uten, som bryteren «Med
logoen til …» i veiviseren, og valget lagres med signeringen. Fylles et utkast
ut på nytt (envelopeId) uten letterhead, beholdes valget det hadde. En egen mal har
sitt eget utseende (letterhead gjelder ikke den); bruker den organisasjonens
logo, får den også brevhodets banner og plassering når den ikke har egne. Et
personlig arbeidsområde har ikke brevhode. Bare en administrator endrer
brevhodet, i appen.
Merkevaren: GET /brand og PUT /brand
Organisasjonens «Merkevare» (Organisasjon › Merkevare i appen): farger
(#rrggbb), fonter, topptekst og bunntekst i tre deler, størrelser,
vannmerke, forside og firmaopplysningene brikkene skriver. Egne maler bruker
den, og standardmalene tar farger, fonter og logostørrelse når standards er
på. GET /brand svarer med merkevaren, fontene som følger med (families),
organisasjonens egne fonter (fonts) og brikkene (chips).
PUT /brand legger det som sendes, oppå det som er lagret: et felt som ikke
er med, blir stående, og null fjerner en gruppe. Tekstfargen må ha kontrast
minst 4,5:1 mot hvitt og hovedfargen minst 3:1 (422 brand_contrast), og en
font som verken følger med eller er organisasjonens egen, er 404. Det krever
en nøkkel med full tilgang (403 read_only_key ellers), og en personlig nøkkel
må kunne administrere organisasjonen (403 admin_key_required); en tilkobling
kan ikke endre merkevaren (403 key_required). Egne fonter lastes opp i appen,
der den som laster opp, bekrefter retten til å bygge dem inn.
curl -X PUT "$RS/brand" -H "Authorization: Bearer $RS_KEY" -H "Content-Type: application/json" \
-d '{"colors": {"primary": "#1f4d3a", "accent": "#c8a24a"}, "fonts": {"body": "Charis SIL", "heading": "Vollkorn"},
"header": {"left": "{firma}", "right": "Org.nr. {orgnr}"}, "footer": {"left": "{tittel}", "right": "{side}"},
"watermark": "KONFIDENSIELT", "cover": true}'
En egen mal kan ha sin egen utgave av hver gruppe i appearance.brand, også
i Markdown-hodet:
appearance:
brand:
colors: { primary: "#1d2b53", accent: "#ff5a1f" }
fonts: { body: Lato, heading: League Spartan }
footer: { left: "{firma}", right: "{side}" }
watermark: ""
En gruppe som ikke er med, er merkevarens; watermark: "" er uten vannmerke.
Eksempel: samboeravtale til to personer
En agent med Hauks personlige nøkkel får «send en samboeravtale til Hauk og Martine»:
# 1. Finne malen
curl -H "Authorization: Bearer $RS_KEY" "$RS/document-templates?q=samboer"
# → { "templates": [{ "ref": "standard:samboer", … }] }
# 2. Spørsmålene (og «Før du begynner», som agenten sier fra om)
curl -H "Authorization: Bearer $RS_KEY" "$RS/document-templates/standard:samboer"
# 3. Sjekke det agenten vet så langt; missing er oppfølgingsspørsmålene
curl -X POST "$RS/document-templates/standard:samboer/check" -H "Authorization: Bearer $RS_KEY" \
-H "Content-Type: application/json" \
-d '{"values": {"fields": {"samboer_fra": "2024-08-01", "bolig": "Vi leier boligen",
"bolig_adresse": "Bjerkeveien 12, 0480 Oslo"},
"parties": {"samboer": [{"me": true}, {"name": "Martine Berg"}]}}}'
# → missing: samboer.1.email, leiekontrakt («når «Boligen» er «Vi leier boligen»»), utgifter
# 4. Når check sier ok: fylle ut til et utkast, med melding og frist
curl -X POST "$RS/document-templates/standard:samboer/fill" -H "Authorization: Bearer $RS_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: samboer-hauk-martine" \
-d '{"values": {"fields": {"samboer_fra": "2024-08-01", "bolig": "Vi leier boligen",
"bolig_adresse": "Bjerkeveien 12, 0480 Oslo", "leiekontrakt": {"other": "oss begge"},
"utgifter": "Vi deler likt"},
"parties": {"samboer": [{"me": true}, {"name": "Martine Berg", "email": "martine@example.no"}]}},
"title": "Samboeravtale Hauk og Martine", "message": "Her er avtalen vi snakket om.", "expiresInDays": 14}'
# → 201 { "id": "1b7c…", "status": "draft", "signers": [{ "name": "Hauk Olaussen", … }, { "name": "Martine Berg", … }] }
# 5. Sende, etter at personen har sagt ja
curl -X POST "$RS/envelopes/1b7c…/send" -H "Authorization: Bearer $RS_KEY"
Egne maler i Markdown
En egen mal skrives i samme Markdown som standardmalene: et YAML-hode mellom to
linjer med ---, og så teksten.
---
name: Leie av parkeringsplass
summary: Utleie av én plass i garasjeanlegget.
titlePattern: Parkeringsplass {{plass}}
roles:
- {key: utleier, label: Utleier, signs: true, fields: [email]}
- {key: leier, label: Leier, signs: true, fields: [email, phone]}
variables:
- {key: plass, label: Plassnummer, type: text, required: true}
- {key: leie, label: Leie per måned, type: money, required: true, min: 1}
- {key: lading, label: Med lading, type: boolean}
---
# Leie av parkeringsplass
::: parter utleier
::: parter leier
§ Plassen
Leier får bruke plass {{plass}} for {{leie}} i måneden.
::: hvis lading
Plassen har lader. Strømmen betales etter måler.
:::
::: signatur
Hodet har name (påkrevd, høyst 120 tegn), summary (beskrivelsen),
titlePattern, appearance (useOrgLogo, logoAlign: left, center
eller right, header: first eller every, og logoAssetId og
bannerAssetId for bilder organisasjonen har lastet opp), roles,
variables, sums og example, med feltene fra innholdet (key, label,
type, required, help, group, options, default, min, max,
unit, columns, maxRows, vat, roles, others; for en rolle help,
signs, pays, min, max og fields). min: 0 er en part som kan
utelates, også en som signerer: legg partslisten og brikkene i den ::: hvis-delen
som handler om parten, så er den med nøyaktig når delen vises. Minst én rolle
som signerer, må ha min 1 eller mer. pays: true går bare på en rolle som
signerer, og betyr at partene er forbrukere som betaler: knappen deres sier
«Signer med plikt til å betale», den signerte PDF-en legges ved deres egen
e-post (se Signatar som betaler), og
signatarene har "pays": true. Nøklene som bare standardmalene har (slug,
version, category, audience, legalBasis, reviewedBy, warnings, keywords, rank),
leses forbi med en merknad, så en standardmal fra ?format=markdown eller
fra fila kan brukes som den er. En ny mal i en bedrift får organisasjonens logo
når hodet ikke nevner et bilde eller useOrgLogo, som i appen;
appearance: {useOrgLogo: false} gir en mal uten den (også med content).
Teksten:
| Skrives | Blir |
|---|---|
# , ## , ### |
overskrift |
§ Tittel, §§ Tittel, §§§ Tittel |
nummerert punkt på nivå 1–3; det varer til neste § på samme eller høyere nivå, til slutten av delen (::: hvis) det står i, eller til ::: slutt § |
§ alene |
et punkt uten tittel (nummeret står foran første avsnitt) |
::: slutt §, ::: slutt §§, ::: slutt §§§ |
punktet på det nivået, og punktene inni det, slutter her; det som følger, står på nivået over (etter alle punktene med ::: slutt §) |
{{pris}}, {{selger.name}}, {{linjer.total}}, {{linjer.sum.antall}} |
svaret, et partsfelt (rolle med høyst én part), tall fra en tabell |
::: hvis nøkkel … ::: |
vises når ja-/nei-spørsmålet er ja, eller feltet er fylt ut |
::: hvis ikke nøkkel … ::: |
vises når det ikke er ja, eller ikke fylt ut |
::: hvis nøkkel = verdi / ::: hvis nøkkel != verdi … ::: |
vises når valget er (ikke er) verdien |
::: parter rolle |
partene i rollen |
::: rader nøkkel |
tabellen av en rows-variabel |
::: signatur, ::: sideskift, --- |
signaturfeltet, sideskift, strek |
**fet**, *kursiv*, [tekst](https://…), - punkt, 1. punkt, | tabell | |
som i Markdown (tabeller med overskriftsrad) |
- punkt og under det - underpunkt (eller 1. underpunkt) |
en liste inni et listepunkt: et punkt med innrykk under et annet hører til det, i så mange nivåer som trengs |
En linje som starter med § , er alltid et punkt, og en del (::: hvis) kan
ikke ligge inni en annen. En del som starter med et punkt på samme nivå som
punktet før den (§ Leie … ::: hvis moblert / § Møbler / :::), står ved
siden av det punktet og avslutter det, som i editoren; en del uten punkter
etter et punkt havner inni punktet, med mindre ::: slutt § står foran den.
§ Leieforholdet
Leien er {{leie}} i måneden.
::: hvis moblert
§ Møbler
Møblene på listen følger med.
:::
§ Depositum
Tre måneders leie.
::: slutt §
::: parter leier
POST /document-templates/validate{ markdown }→{ ok, problems, questions }, og lagrer ingenting. Hvert problem harline(den første---er linje 1) når det kan sies,path,code,messageogseverity:error(kan ikke lagres: Markdown-en kan ikke leses, en ukjent type, en nøkkel som spør etter fødselsnummer),not_ready(lagres som utkast, men kan ikke publiseres: en brikke til en variabel som ikke finnes, ingen part som signerer, et tomt dokument) ellernote(stopper ingenting). For feil i YAML-syntaksen peker linjen på der listen eller objektet feilen står i, starter.POST /document-templates{ markdown }(eller{ content, name, summary }som editoren lagrer det) →201med malen som utkast,ok,problemsogquestions.errorgir422 template_invalidmedproblems, og ingenting lagres. Et navn som er tatt gir409.Idempotency-Keygir samme mal tilbake.PUT /document-templates/{id}{ markdown }lagrer utkastet (og navn og beskrivelse fra hodet). Hadde utkastet noe Markdown ikke kan si, er det borte, og svaret sier hva somnote-problemer medcode: lost_on_save.pathpå dem peker inn i utkastet slik det var før lagringen (somnotesfra<id>@draft?format=markdown-jsonfør lagringen), ikke i det nye. Hent dem før du lagrer, og spør personen om det er greit at det forsvinner.contentleses strengt: et felt innholdet ikke har, gir422 template_invalid.POST /document-templates/{id}/publishgjør utkastet til versjonen som fylles ut:201{ ref: "<id>@2", version, publishedAt, created },200når utkastet er versjonen som gjelder,422 template_not_readymedproblems.DELETE /document-templates/{id}arkiverer malen (204). Det kan ikke angres, men utkastet (<id>@draft, også som Markdown) kan fortsatt hentes av den som kunne endre malen.
Det editoren kan lage som Markdown ikke kan si, er med i malen, men ikke i
?format=markdown: bilder i teksten, understreking, linjeskift inni et avsnitt
(skrives som mellomrom), justering, tomme avsnitt, flere avsnitt i et
listepunkt, flere avsnitt eller lister i en celle, en tabell uten
overskriftsrad og kolonnebredder. Punkter, deler, det som står etter et punkt
(::: slutt §) og lister i lister kommer tilbake der de stod. Hver ting
står som en kommentar øverst i hodet, og Relation-Sign-Markdown-Notes sier
hvor mange (eller som notes med ?format=markdown-json). Lagres
Markdown-en tilbake, er de borte, og PUT sier det (lost_on_save).
Idempotens
Send Idempotency-Key på POST /envelopes, POST /document-templates/{ref}/fill
og POST /document-templates når forespørselen kan bli sendt to ganger
(tidsavbrudd, nytt forsøk fra en kø):
- Samme nøkkel og samme forespørsel innen 24 timer gir samme signering
(eller samme mal) tilbake, med headeren
Idempotent-Replayed: true. Det sendes ingen nye e-poster. - «Samme forespørsel» betyr samme payload og samme PDF (SHA-256), uansett om den kom som multipart eller JSON.
- Samme nøkkel med en annen forespørsel gir
422. - Mens den første forespørselen pågår, gir et nytt forsøk
409; prøv igjen. - Feiler forespørselen (for eksempel
422), er nøkkelen ikke brukt opp. - Nøkler gjelder per API-nøkkel og glemmes etter 24 timer.
Bruk en nøkkel som sier hva som sendes, for eksempel
relation-os:stalbygg:change_order:<id>:rev<revisjon>.
Webhooker
En administrator registrerer en mottaker (POST /api/webhooks, se under).
Relation Sign sender da en POST til adressen for hver hendelse i
organisasjonen.
Levering
POST <din adresse>
Content-Type: application/json
User-Agent: RelationSign-Webhooks/1
Relation-Sign-Event: envelope.completed
Relation-Sign-Delivery: 5d0e… (samme på alle forsøk)
Relation-Sign-Signature: t=1760870400,v1=3b1f…
{ "id": "…", "type": "envelope.completed", "createdAt": "…", "data": { "envelope": { … }, "signer": { … }, "comment": { … } } }
- Bare organisasjoner med API i planen får webhooker. Mister planen API-et,
legges ingenting nytt i køen, og leveringer som venter, settes til
failedmed feilen «planen til organisasjonen har ikke lenger API og webhooks». - Svar med 2xx innen 10 sekunder. Alt annet, også videresending (3xx), er en feil.
- Nye forsøk etter 30 s, 1, 2, 4, 8 ... minutter (maks 6 timer mellom), totalt
10 forsøk. Deretter
failedi leveringsloggen. Hver ventetid spres tilfeldig med inntil 20 % begge veier. - Svarer dere
429eller503medRetry-After(sekunder eller dato), venter vi så lenge, men aldri over 6 timer. 410 Goneog413gir ingen nye forsøk: leveringen blirfailedmed en gang.- Kroppen er den samme på alle forsøk;
ti signaturen er tidspunktet for forsøket. - Mottakeren får 10 sekunder (oppkobling, TLS og svar). Vi leser bare den første 1 KB av svaret, og den vises i leveringsloggen.
- Rekkefølgen er ikke garantert. Bruk
data.envelope.statusog tidspunktene, og dedupliser påid. - Adressen må være https og kan ikke peke på interne adresser (private nett, loopback, link-local, metadata). Det sjekkes på nytt ved hver levering: alle A- og AAAA-adressene navnet peker på må være offentlige (én intern er nok til å avvise), vi kobler til nettopp den adressen vi sjekket, og videresendinger følges aldri.
- Leveringsloggen viser status, HTTP-kode, varighet, antall forsøk, neste forsøk, hele kroppen som ble sendt og starten av svaret. Leveringer slettes etter 90 dager, siden de inneholder navn og e-postadresser.
Pause, «Send på nytt» og automatisk avslåing
- Pause: en webhook på pause får ingenting. Hendelser som skjer mens den
er på pause, og leveringer som ventet på et nytt forsøk da den ble satt på
pause, står som
skipped(«Hoppet over») i leveringsloggen. De sendes ikke når webhooken slås på igjen; bare hendelser etter det sendes. Testknappen virker også på pause, så dere kan sjekke mottakeren først. - Send på nytt: hvilken som helst levering (feilet, levert eller hoppet
over) kan sendes på nytt. Det blir en ny levering med ny
Relation-Sign-Delivery, men samme hendelse: samme kropp og sammeid, så mottakeren kan deduplisere. Den prøves med en gang og deretter som vanlig. Webhooken må være på (ellers409). - Automatisk avslåing: når de siste 15 forsøkene på rad mot en webhook har
feilet, og den første av dem er minst tre døgn gammel, slår Relation Sign av
webhooken og sender en e-post til alle administratorene i organisasjonen.
Integrasjoner viser et banner med siste feil og en knapp for å slå den på
igjen. Leveringer som ventet, og hendelser mens den er av, står som
skippedog sendes ikke senere. Ett vellykket svar (2xx) nullstiller tellingen.
Bytte hemmelighet
«Bytt hemmelighet» (POST /api/webhooks/{id}/rotate-secret) gir en ny
hemmelighet, som vises én gang. I 24 timer etterpå signeres hver melding med
begge: headeren har da to v1, først med den nye og så med den gamle:
Relation-Sign-Signature: t=1760870400,v1=<ny>,v1=<gammel>
Mottakeren godtar meldingen hvis én av v1 stemmer med hemmeligheten den har
(eksemplene under gjør det). Bytt hemmeligheten hos mottakeren innen 24 timer;
etterpå er det bare den nye som signerer. Bytter dere igjen innen 24 timer,
slutter den eldste å virke med en gang.
Verifisere signaturen
v1 er heks av HMAC-SHA256 med hemmeligheten (whsec_...) over
<t>.<rå kropp>. Sjekk alltid den rå kroppen før du parser den, avvis t
som er mer enn fem minutter fra nå, og godta meldingen hvis én av v1
stemmer (det kan være to rett etter at hemmeligheten er byttet).
Svar 401 når signaturen ikke stemmer. Knappen «Sjekk signaturen» under
Integrasjoner sender én melding med riktig signatur og én med feil, og sier
fra hvis mottakeren godtar begge.
Go:
package relationsign
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"net/http"
"strconv"
"strings"
"time"
)
// VerifyRequest reads the body and checks Relation-Sign-Signature.
func VerifyRequest(r *http.Request, secret string) ([]byte, error) {
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
return nil, err
}
var ts string
var sigs []string
for _, part := range strings.Split(r.Header.Get("Relation-Sign-Signature"), ",") {
k, v, _ := strings.Cut(strings.TrimSpace(part), "=")
switch k {
case "t":
ts = v
case "v1":
sigs = append(sigs, v)
}
}
sec, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return nil, errors.New("missing timestamp")
}
if d := time.Since(time.Unix(sec, 0)); d > 5*time.Minute || d < -5*time.Minute {
return nil, errors.New("timestamp outside tolerance")
}
m := hmac.New(sha256.New, []byte(secret))
m.Write([]byte(ts + "."))
m.Write(body)
want := hex.EncodeToString(m.Sum(nil))
for _, s := range sigs {
if hmac.Equal([]byte(s), []byte(want)) {
return body, nil
}
}
return nil, errors.New("signature does not match")
}
TypeScript (Node):
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyRelationSign(rawBody: Buffer, header: string | undefined, secret: string): boolean {
if (!header) return false;
let ts = "";
const sigs: string[] = [];
for (const part of header.split(",")) {
const [k, v] = part.trim().split("=", 2);
if (k === "t") ts = v;
if (k === "v1") sigs.push(v);
}
const sec = Number(ts);
if (!Number.isInteger(sec) || Math.abs(Date.now() / 1000 - sec) > 300) return false;
const want = createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest();
return sigs.some((s) => {
const got = Buffer.from(s, "hex");
return got.length === want.length && timingSafeEqual(got, want);
});
}
// Express: app.post("/hook", express.raw({ type: "application/json" }), (req, res) => {
// if (!verifyRelationSign(req.body, req.get("Relation-Sign-Signature"), process.env.RS_WEBHOOK_SECRET!)) return res.sendStatus(401);
// const event = JSON.parse(req.body.toString("utf8"));
// res.sendStatus(204);
// });
Python:
import hashlib
import hmac
import time
def verify_relation_sign(raw_body: bytes, header: str | None, secret: str, tolerance: int = 300) -> bool:
if not header:
return False
ts, sigs = "", []
for part in header.split(","):
k, _, v = part.strip().partition("=")
if k == "t":
ts = v
elif k == "v1":
sigs.append(v)
if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
return False
want = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(s, want) for s in sigs)
# Flask: body = request.get_data() (rå bytes, før du parser JSON)
# if not verify_relation_sign(body, request.headers.get("Relation-Sign-Signature"), os.environ["RS_WEBHOOK_SECRET"]):
# abort(401)
C#:
using System.Security.Cryptography;
using System.Text;
public static class RelationSign
{
public static bool Verify(byte[] rawBody, string? header, string secret, int toleranceSeconds = 300)
{
if (string.IsNullOrEmpty(header)) return false;
string? ts = null;
var sigs = new List<string>();
foreach (var part in header.Split(','))
{
var kv = part.Trim().Split('=', 2);
if (kv.Length != 2) continue;
if (kv[0] == "t") ts = kv[1];
else if (kv[0] == "v1") sigs.Add(kv[1]);
}
if (!long.TryParse(ts, out var sec)) return false;
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - sec) > toleranceSeconds) return false;
using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var want = mac.ComputeHash(Encoding.UTF8.GetBytes(ts + ".").Concat(rawBody).ToArray());
foreach (var s in sigs)
{
byte[] got;
try { got = Convert.FromHexString(s); } catch (FormatException) { continue; }
if (CryptographicOperations.FixedTimeEquals(got, want)) return true;
}
return false;
}
}
// ASP.NET Core: les Request.Body til byte[] (EnableBuffering) før du parser JSON.
Hendelser
| Type | Når | data |
|---|---|---|
envelope.sent |
signeringen er sendt | envelope |
signer.ready |
det er turen til en signatar med delivery: "none" (med en gang ved parallell signering, når forrige har signert ved fast rekkefølge); hent lenken med /link |
envelope, signer |
signer.viewed |
en signatar åpnet lenken første gang | envelope, signer |
signer.signed |
en signatar signerte | envelope, signer |
signer.declined |
en signatar avviste (se signer.declineReason) |
envelope, signer |
envelope.completed |
alle har signert og PDF-en er forseglet; hent den med /signed |
envelope (med sealedSha256) |
envelope.declined |
signeringen er avvist (kommer sammen med signer.declined) |
envelope, signer |
envelope.expired |
fristen gikk ut | envelope |
envelope.cancelled |
avsenderen trakk den tilbake | envelope |
envelope.withdrawn |
en signatar trakk signaturen sin; signeringen er stoppet | envelope, signer (med withdrawReason) |
comment.added |
ny kommentar på dokumentet | envelope, signer (hvis en signatar skrev), comment |
comment.withdrawn |
den som skrev en kommentartråd, fjernet den (bare mulig før noen andre har svart eller signert); fjern den hos dere | envelope, signer (hvis en signatar fjernet), comment (id, authorName, version) |
signature.invalidated |
en signatur ble nullstilt av en kommentar eller ny versjon | envelope, signer (med invalidatedReason) |
envelope.version_added |
avsenderen lastet opp en ny versjon | envelope (med ny version og originalSha256) |
signer.updated |
avsenderen endret navn, e-post, telefon, metode eller språk på en signatar som ikke har signert; ny e-post eller telefon gir ny lenke | envelope, signer |
envelope.extended |
avsenderen forlenget fristen | envelope (med ny expiresAt) |
envelope.archived |
signeringen ble arkivert, av en person eller automatisk; ingenting er slettet | envelope (med archivedAt) |
envelope.restored |
signeringen ble hentet fra arkivet | envelope |
envelope.deleted |
signeringen ble slettet permanent (av avsenderen, eller automatisk 90 dager etter at den ble avsluttet, når ingen har signert); GET gir 404 etterpå |
envelope slik den var |
envelope.documents_deleted |
dokumentene ble slettet 5 år etter at signeringen ble avsluttet; signeringen står igjen som verifiseringspost, og nedlasting gir 410 med koden documents_deleted |
envelope slik den var |
ping |
testknappen | { webhookId, message } |
data.envelope:
{
"id": "…", "externalRef": "relation-os:stalbygg:change_order:7f3c", "title": "…",
"status": "completed", "signingOrder": "parallel", "version": 1,
"originalSha256": "…", "sealedSha256": "…",
"createdAt": "…", "sentAt": "…", "completedAt": "…", "expiresAt": "…", "archivedAt": null,
"folderId": null,
"sender": { "id": "…", "name": "Per Prosjektleder", "email": "pl@straye.no" },
"signers": [Signer]
}
Signer: { id, position, name, email, method, status, viewedAt, signedAt, declinedAt, declineReason?, withdrawnAt, withdrawReason?, invalidatedAt, invalidatedReason?, delivery }.
comment: { id, authorKind ("sender" | "signer"), authorName, body, version, page?, x?, y?, quote? }. quote er teksten kommentaren markerer, når den
markerer et utdrag.
Tilkoblinger fra katalogen (OAuth 2.0)
Apper i integrasjonskatalogen (Zapier, Make, Tripletex og så videre) kobles til
med OAuth 2.0 i stedet for en API-nøkkel. En administrator godkjenner appen for
organisasjonen sin, og appen får et tilgangstoken som brukes på /api/v1
akkurat som en nøkkel: Authorization: Bearer rs_oat_.... Bare apper Relation
Labs har satt opp (OAUTH_CLIENTS) er klienter. Designet står i
docs/specs/2026-10-24-integrasjoner-og-katalog-design.md.
| Flyt | authorization code med PKCE (S256 er påkrevd) |
| Autorisasjon | https://<domene>/koble/autoriser (samtykkesiden i appen) |
| Token | POST https://<domene>/api/oauth/token (skjemadata, klienthemmelighet med Basic eller client_id/client_secret) |
| Tilbakekalling | POST https://<domene>/api/oauth/revoke (RFC 7009) |
| Tilganger | envelopes:read, envelopes:write, webhooks; tom scope gir alt klienten kan få |
redirect_urimå være nøyaktig en av klientens. Er den ukjent, sendes brukeren ingen steder. Andre feil sendes tilbake til appen mederrorogstate.- Bare en administrator kan godta. En
senderser siden og kan avslå (error=access_denied). - Koden kan brukes én gang og varer i 60 sekunder. Brukes den igjen, trekkes tokenene den ga tilbake.
- Tilgangstokenet varer en time. Refresh token (
rs_ort_...) varer 180 dager og byttes ved hver bruk. Kommer et byttet refresh token igjen, er tilgangen fra den godkjenningen trukket tilbake. - Flere godkjenninger av samme app i samme organisasjon er én tilkobling med hver sin tokenfamilie. «Koble fra» under Integrasjoner, Tilkoblinger stopper alle med en gang og sletter webhookene tilkoblingen laget.
- Mangler tokenet tilgangen kallet trenger, svarer API-et
403medcode: "insufficient_scope". Skriving kreverenvelopes:write, lesingenvelopes:read(ellerenvelopes:write),/webhookskreverwebhooks. - Tilkoblinger er en del av API-et: de krever API i planen, som API-nøkler.
På Løs vekt uten API-tillegget kan ingen godkjenne en tilkobling, og et
tilgangstoken får
403. Signeringene betales som ellers. - Signeringer en tilkobling lager, har
source: { kind: "connection", app, appName }, og revisjonssporet sier «Tilkobling «Zapier»». Andre signeringer har ingensource.
# Bytte koden mot tokens
curl -X POST "https://sign.relationlabs.no/api/oauth/token" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code -d code="$CODE" \
-d redirect_uri="$REDIRECT_URI" -d code_verifier="$VERIFIER"
# {"access_token":"rs_oat_...","token_type":"Bearer","expires_in":3600,
# "refresh_token":"rs_ort_...","scope":"envelopes:read envelopes:write webhooks"}
Hvem er jeg: GET /me
Organisasjonen og tilkoblingen (app og tilganger). Med en personlig nøkkel:
nøkkelen og eieren. En API-nøkkel fra Integrasjoner får 404.
Abonnere: POST /webhooks og DELETE /webhooks/{id}
For REST hooks (Zapier, Make): { url, events? } gir 201 { webhook, secret }.
Bare med et tilgangstoken; en API-nøkkel får 404 (webhooker for nøkler lages
under Integrasjoner). Webhooken hører til tilkoblingen, teller ikke mot
organisasjonens ti (høyst 20 per tilkobling), vises ikke under Integrasjoner,
Webhooker, og slettes når tilkoblingen kobles fra. Den får hendelser også på
Løs vekt, men ikke når organisasjonen ikke lenger er med i piloten. En
tilkobling kan bare slette sine egne (404 ellers).
Tilkoblinger er en pilot: bare organisasjoner med funksjonen integrations
kan koble til.
Administrasjon (innlogget, rolle admin)
Disse brukes av Relation Sign sitt eget nettgrensesnitt med sesjonscookie, ikke med API-nøkkel. Alt er avgrenset til administratorens organisasjon.
API-nøkler
| Metode | Sti | Kropp | Svar |
|---|---|---|---|
| GET | /api/api-keys |
{ apiKeys: ApiKey[] } |
|
| POST | /api/api-keys |
{ name, fallbackUserId?, scope? ("full" | "read"), expiresAt? } |
201 { apiKey: ApiKey, key: "rs_live_..." } (nøkkelen vises bare her). Ukjent tilgang eller utløpsdato i fortiden eller mer enn tre år fram gir 422 |
| PATCH | /api/api-keys/{id} |
{ fallbackUserId: uuid | null } |
200 { apiKey }. Reserveavsender må være en aktiv bruker i organisasjonen (ellers 422); fremmed eller tilbakekalt nøkkel er 404 |
| DELETE | /api/api-keys/{id} |
204 (tilbakekaller; nøkkelen blir stående i listen) |
ApiKey = { id, name, prefix, createdBy: { id, name }, createdAt, lastUsedAt, lastUsedIp, revokedAt, fallbackUser, scope, expiresAt }. Bruk registreres
høyst én gang i minuttet per nøkkel, og med en gang når IP-adressen endrer seg.
Personlige nøkler er ikke med her. Hver bruker (alle roller) har sine egne, og en administrator ser alle:
| Metode | Sti | Kropp | Svar |
|---|---|---|---|
| GET | /api/me/api-keys |
{ apiKeys: ApiKey[] }, egne |
|
| POST | /api/me/api-keys |
{ name, scope? ("full" | "read" | "admin"), expiresAt? } |
201 { apiKey, key }; 403 uten API i planen, og for admin når du ikke er administrator |
| DELETE | /api/me/api-keys/{id} |
204 tilbakekaller (?permanent=true sletter en tilbakekalt eller utløpt); en annens nøkkel er 404 |
|
| GET | /api/personal-keys |
administrator: { apiKeys: ApiKey[] } for alle, createdBy er eieren |
|
| DELETE | /api/personal-keys/{id} |
administrator: 204 tilbakekaller |
Webhooker
| Metode | Sti | Kropp | Svar |
|---|---|---|---|
| GET | /api/webhooks |
{ webhooks: Webhook[] } |
|
| POST | /api/webhooks |
{ url, events? } (tom eller manglende events = alle) |
201 { webhook: Webhook, secret: "whsec_..." } (hemmeligheten vises bare her) |
| PATCH | /api/webhooks/{id} |
{ url?, events? } (felt som mangler, beholdes; [] = alle) |
200 { webhook }. Hemmeligheten beholdes |
| DELETE | /api/webhooks/{id} |
204 (sletter også leveringsloggen) |
|
| POST | /api/webhooks/{id}/pause |
200 { webhook } (se «Pause» over) |
|
| POST | /api/webhooks/{id}/resume |
200 { webhook }. Slår på en webhook som er satt på pause eller slått av automatisk, og nullstiller feiltellingen |
|
| POST | /api/webhooks/{id}/rotate-secret |
200 { webhook, secret: "whsec_..." } (hemmeligheten vises bare her; den gamle signerer i 24 timer til) |
|
| GET | /api/webhooks/events |
{ events: string[] } (katalogen over) |
|
| POST | /api/webhooks/{id}/test |
{ delivery: Delivery } (ett forsøk med ping, med en gang; virker også på pause) |
|
| POST | /api/webhooks/{id}/test-signature |
{ verdict, valid: Delivery, invalid: { statusCode, error, responseBody, durationMs } }. Sender en ping med riktig signatur og en med feil. verdict er ok (riktig godtatt, feil avvist), accepts_invalid, valid_failed eller inconclusive. Bare den riktige havner i leveringsloggen |
|
| GET | /api/webhooks/{id}/deliveries?status= |
{ deliveries: Delivery[] } (siste 50, uten kroppen; status filtrerer) |
|
| GET | /api/webhooks/{id}/deliveries/{deliveryId} |
{ delivery: Delivery & { payload, responseBody } } |
|
| POST | /api/webhooks/{id}/deliveries/{deliveryId}/redeliver |
{ delivery: Delivery } («Send på nytt»; 409 når webhooken ikke er på) |
Webhook = { id, url, events, active, state ("active" | "paused" | "disabled" | "off"), pausedAt, disabledAt, disabledReason, consecutiveFailures, failingSince, secretRotatedAt, previousSecretExpiresAt (bare mens den gamle hemmeligheten signerer), createdAt, updatedAt }. off er en webhook Relation
OS har skrudd av ved frakobling.
Delivery = { id, eventId, eventType, envelopeId, status ("pending" | "delivered" | "failed" | "skipped"), attempts, maxAttempts, lastStatusCode, lastError, durationMs, nextAttemptAt (bare når pending), deliveredAt, redeliveryOf, createdAt }.
Høyst 10 webhooker per organisasjon.
Varsler i Slack og Teams
Kanaler som får en kort melding med tittel, hvem det gjelder og en lenke til
signeringen. Bare for integrasjonspiloten, og uten krav om API i planen.
Adressen er en innkommende webhook fra Slack (https://hooks.slack.com/…)
eller en arbeidsflyt i Teams (*.logic.azure.com, *.powerplatform.com).
Meldingene går i samme kø som webhookene, med de samme nye forsøkene og
automatisk avslåing, men er ikke signert. Teams får et Adaptive Card.
| Metode | Sti | Kropp | Svar |
|---|---|---|---|
| GET | /api/chat-channels |
{ channels: Channel[], events, defaultEvents } |
|
| POST | /api/chat-channels |
{ kind: "slack" | "teams", label?, url, events? } (tom events = defaultEvents) |
201 { channel } |
| PATCH | /api/chat-channels/{id} |
{ label?, events? } (minst én hendelse) |
200 { channel } |
| DELETE | /api/chat-channels/{id} |
204 |
|
| POST | /api/chat-channels/{id}/test |
{ delivered, error } |
|
| POST | /api/chat-channels/{id}/resume |
200 { channel } (etter automatisk avslåing) |
Channel = { id, kind, label, address (vertsnavn og de fire siste tegnene; hele adressen vises aldri igjen), events, state, failingSince, disabledReason, createdAt }. Hendelsene en kanal kan få: envelope.sent, signer.signed,
envelope.completed, envelope.declined, envelope.expired og
comment.added. Uten valg får den envelope.completed, envelope.declined,
envelope.expired og comment.added. Høyst 10 kanaler per organisasjon.
SharePoint (integrasjonspiloten)
Organisasjonens tilkobling, standardmappe og valg per signering
(docs/specs/2026-10-25-sharepoint-arkiv-design.md). Innlogget; connect,
default og DELETE /api/sharepoint bare for administratorer.
| Metode | Sti | Kropp | Svar |
|---|---|---|---|
| GET | /api/sharepoint |
{ available, organization: { connected, name, email, default, connectedAt, connectionId }, me: { connected, name, email } } |
|
| POST | /api/sharepoint/connect |
{ return } |
{ url }: send nettleseren dit (organisasjonen) |
| POST | /api/sharepoint/me/connect |
{ return } |
{ url } (egen konto). Første forsøk er stille (prompt=none) |
| GET | /api/sharepoint/callback |
303 tilbake til return med microsoft=koblet eller microsoft-feil=… |
|
| PUT | /api/sharepoint/default |
{ driveId, itemId } |
{ default: Target } |
| DELETE | /api/sharepoint |
204 (fjerner også alle egne kontoer) |
|
| DELETE | /api/sharepoint/me |
204 |
|
| GET | /api/sharepoint/browse?as=org|me&q=&site=&drive=&item=&next= |
{ entries: [{ kind, id, name, webUrl, driveId }], next, here: Target } |
|
| POST | /api/sharepoint/folders |
{ as, driveId, parentId, name } |
201 { folder: Target } |
| GET | /api/envelopes/{id}/sharepoint |
{ filing } |
|
| PUT | /api/envelopes/{id}/sharepoint |
{ mode: "default" | "own" | "off", driveId?, itemId? } |
{ filing }; 409 når den er lastet opp |
| POST | /api/envelopes/{id}/sharepoint/retry |
{ filing } |
Target = { driveId, itemId, siteName, path, webUrl }. filing = { mode, target, default, mine, chosenBy, uploadedAt, fileUrl, uploadedPath, usedDefault, note, attempts, nextAttemptAt, lastError, failedAt }. 409 med
code: "microsoft_account_required" betyr at personen må koble til sin egen
Microsoft-konto først; microsoft_reconnect at innloggingen er utløpt.
Plattform-API (bare Relation OS)
Brukes av Relation OS for å aktivere, følge og avslutte koblingen til en
organisasjon. Alle kall har Authorization: Bearer <PLATFORM_KEY>; feil eller
manglende nøkkel er 401. Uten PLATFORM_KEY på serveren finnes ikke rutene
(404). Pakker er komet, planet eller stjerne.
| Metode | Sti | Kropp | Svar |
|---|---|---|---|
| POST | /api/platform/organizations |
{ relationOsCompanyId, relationOsTenant?, name, orgNumber?, osPackage, webhookUrl?, rotateKey? } |
se under |
| PUT | /api/platform/organizations/{id}/plan |
{ osPackage } |
204. Planen settes til relation_os med pakken (opprettet av relation_os). 409 hvis organisasjonen ikke er koblet |
| DELETE | /api/platform/organizations/{id}/link |
204. Koblingen avsluttes, planen går til Løs vekt (hvis den var relation_os), Relation OS-nøkkelen tilbakekalles og webhooken skrus av. Organisasjonen, brukerne og dokumentene blir værende. Å koble fra noe som ikke er koblet gjør ingenting |
POST /api/platform/organizations er idempotent på relationOsCompanyId:
- Nytt selskap, ukjent organisasjonsnummer:
201 { status: "linked", organizationId, apiKey, webhookSecret? }. Organisasjonen opprettes på planenrelation_os, med en tjenestebruker «Relation OS» (administrator) som eier API-nøkkelen og er reserveavsender.webhookSecretfølger med nårwebhookUrler gitt. Nøkkelen og hemmeligheten vises bare her. - Allerede koblet:
200 { status: "linked", organizationId }uten nye hemmeligheter. MedrotateKey: truebyttes API-nøkkelen (og webhook-hemmeligheten nårwebhookUrler gitt), og de nye verdiene returneres. Mangler organisasjonen en nøkkel, for eksempel rett etter en godkjenning, utstedes den nå. - Organisasjonsnummeret finnes på en organisasjon som ikke er koblet:
202 { status: "pending_approval" }. En administrator i organisasjonen får en forespørsel på Organisasjon-siden og godkjenner eller avviser. Ved godkjenning kobles selskapet og planen settes tilrelation_os; neste kall med sammerelationOsCompanyIdgir200med nøkkelen. Ved avvisning svarer kallet403 { status: "rejected" }i sju dager, deretter kan Relation OS spørre på nytt. 409når organisasjonsnummeret bare finnes på organisasjoner koblet til andre selskaper,422for ugyldig innhold.
Administratorenes side av godkjenningen (sesjon, rolle admin):
| Metode | Sti | Kropp | Svar |
|---|---|---|---|
| GET | /api/organization/link-requests |
{ linkRequests: [{ id, companyName, osPackage, packageName, requestedAt }] } |
|
| POST | /api/organization/link-requests/{id} |
{ decision: "approve" | "reject" } |
204; ukjent, avgjort eller fremmed forespørsel er 404 |