Tidvis
Utvecklare · OAuth 2.0

Koppla din app till Tidvis Sign

Bygg en “Koppla till Tidvis Sign”-knapp som låter era kunder ge er app tillgång till deras Tidvis Sign-konto. Authorization code flow med PKCE.

Översikt

Kopplingen sker i tre steg. Slutanvändaren godkänner själv vilka behörigheter din app får till sitt Tidvis Sign-konto, och kan när som helst bryta kopplingen.

  1. 1. Din knapp skickar användaren till Tidvis auktoriseringsadress. Vi visar godkännandesidan /anslut.
  2. 2. Efter godkännande skickas användaren tillbaka till din redirect_uri med en engångskod.
  3. 3. Din server byter koden mot ett access_token och ett refresh_token.

Base URL: https://tidvis.se/api/public/v1. PKCE (code_challenge) är obligatoriskt.

Registrera din app

Registrera appen på /utvecklare/appar. Du anger namn, kontaktmail, hemsida, logotyp, retur-adresser och de behörigheter appen behöver.

  • client_id (prefix tvapp_) är publikt och används i auktoriseringslänken.
  • client_secret (prefix tvsec_) visas en enda gång vid registrering. Spara den säkert och använd den bara server-side.
  • Retur-adresser måste matcha exakt — redirect_uri i länken jämförs mot de adresser du registrerat.
  • Verifierade appar visas med en verifieringsmarkering på godkännandesidan. Kontakta oss för verifiering.

Steg 1 – Auktoriseringslänken

Din “Koppla till Tidvis Sign”-knapp ska peka på GET https://tidvis.se/api/public/v1/oauth/authorize. Alla parametervärden ska vara URL-kodade.

https://tidvis.se/api/public/v1/oauth/authorize
  ?client_id=tvapp_xxxxxxxxxxxxxxxx
  &redirect_uri=https%3A%2F%2Fdin-app.se%2Fintegrationer%2Ftidvis-sign%2Fcallback
  &response_type=code
  &scope=agreements%3Aread%20agreements%3Acreate%20agreements%3Asend
  &state=8f3c1d9a2b
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
ParameterKravBeskrivning
client_idObligatoriskAppens publika id, prefix tvapp_.
redirect_uriObligatoriskMåste matcha en av appens registrerade retur-adresser exakt.
response_typeObligatoriskAlltid code.
scopeObligatoriskMellanslagsseparerad lista. Scopes som appen inte fått tilldelade avvisas.
code_challengeObligatoriskPKCE: base64url(SHA-256(code_verifier)). Minst 20 tecken.
code_challenge_methodObligatoriskS256 (rekommenderas) eller plain.
stateValfriSlumpvärde som skickas tillbaka oförändrat. Använd alltid för CSRF-skydd.

Är användaren inte inloggad skickas hen först till inloggningen och därefter tillbaka till godkännandesidan. Har användaren flera Tidvis Sign-konton väljer hen konto där.

Steg 2 – Callback

Efter godkännande skickas användaren till din redirect_uri:

https://din-app.se/integrationer/tidvis-sign/callback?code=tvac_...&state=8f3c1d9a2b
  • Kontrollera att state matchar det värde du skickade.
  • Koden är engångs och giltig i 10 minuter.
  • Avbryter användaren får du istället ?error=access_denied&error_description=...&state=....

Steg 3 – Byt koden mot ett token

Gör anropet server-side mot POST https://tidvis.se/api/public/v1/oauth/token. Body kan vara JSON eller formulärdata.

curl -X POST https://tidvis.se/api/public/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "tvac_...",
    "code_verifier": "<samma verifier som gav code_challenge>",
    "redirect_uri": "https://din-app.se/integrationer/tidvis-sign/callback",
    "client_id": "tvapp_...",
    "client_secret": "tvsec_..."
  }'

Svar:

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "expires_at": "2026-01-01T12:00:00.000Z",
  "refresh_token": "tvrt_...",
  "scope": "agreements:read agreements:create agreements:send"
}

access_token gäller i 1 timme. Spara refresh_token krypterat — det är det som håller kopplingen levande.

Förnya token

curl -X POST https://tidvis.se/api/public/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "tvrt_...",
    "client_id": "tvapp_...",
    "client_secret": "tvsec_..."
  }'

Varje förnyelse ger ett nytt refresh_token — spara alltid det senaste. Har användaren brutit kopplingen får du invalid_grant.

Använda API:t

Skicka access_token som Bearer-token mot Public API v1.

curl https://tidvis.se/api/public/v1/agreements \
  -H "Authorization: Bearer <access_token>"

Alla endpoints beskrivs i API-dokumentationen och i OpenAPI-referensen.

Återkalla kopplingen

Din app kan återkalla sin egen koppling via POST https://tidvis.se/api/public/v1/oauth/revoke. Enligt RFC 7009 svarar endpointen alltid 200.

curl -X POST https://tidvis.se/api/public/v1/oauth/revoke \
  -H "Content-Type: application/json" \
  -d '{"client_id":"tvapp_...","client_secret":"tvsec_...","token":"tvrt_..."}'

Slutanvändaren kan själv bryta kopplingen under Sign → Inställningar → Anslutna appar. Då slutar både access- och refresh-token att fungera direkt.

Behörigheter (scopes)

Begär bara de behörigheter appen faktiskt behöver — de visas för användaren på godkännandesidan.

ScopeBetydelse
agreements:readLäs avtal
agreements:createSkapa avtal
agreements:sendSkicka avtal
agreements:cancelAvbryt avtal
templates:readLäs avtalsmallar
templates:manageSkapa/uppdatera avtalsmallar
generations:createGenerera avtal från mall
generations:readLäs & ladda ner genererade avtal
billing:checkoutFakturering / checkout
sub_accounts:readLäs underkonton
sub_accounts:manageHantera underkonton
sub_accounts:membersHantera medlemmar i underkonton
bankid:identifyBankID legitimering
bankid:sign:dataBankID – signera text och hash (utan PDF)
mcp:connectMCP-anslutning

Komplett exempel (Node / TypeScript)

PKCE-generering, redirect och callback-hantering.

import crypto from "node:crypto";

const AUTHORIZE = "https://tidvis.se/api/public/v1/oauth/authorize";
const TOKEN = "https://tidvis.se/api/public/v1/oauth/token";
const CLIENT_ID = process.env.TIDVIS_CLIENT_ID!;
const CLIENT_SECRET = process.env.TIDVIS_CLIENT_SECRET!;
const REDIRECT_URI = "https://din-app.se/integrationer/tidvis-sign/callback";
const SCOPES = "agreements:read agreements:create agreements:send";

const b64url = (b: Buffer) => b.toString("base64url");

// 1. Knappen: skapa PKCE + state, spara i session, skicka vidare.
export function startConnect(session: Record<string, string>) {
  const verifier = b64url(crypto.randomBytes(32));
  const challenge = b64url(crypto.createHash("sha256").update(verifier).digest());
  const state = b64url(crypto.randomBytes(16));
  session["tidvis_verifier"] = verifier;
  session["tidvis_state"] = state;

  const url = new URL(AUTHORIZE);
  url.searchParams.set("client_id", CLIENT_ID);
  url.searchParams.set("redirect_uri", REDIRECT_URI);
  url.searchParams.set("response_type", "code");
  url.searchParams.set("scope", SCOPES);
  url.searchParams.set("state", state);
  url.searchParams.set("code_challenge", challenge);
  url.searchParams.set("code_challenge_method", "S256");
  return url.toString(); // redirecta hit
}

// 2 + 3. Callback: validera state, byt kod mot token.
export async function handleCallback(
  requestUrl: string,
  session: Record<string, string>,
) {
  const q = new URL(requestUrl).searchParams;
  if (q.get("error")) throw new Error(q.get("error_description") ?? q.get("error")!);
  if (!q.get("state") || q.get("state") !== session["tidvis_state"]) {
    throw new Error("Ogiltig state");
  }

  const res = await fetch(TOKEN, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      grant_type: "authorization_code",
      code: q.get("code"),
      code_verifier: session["tidvis_verifier"],
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
    }),
  });
  if (!res.ok) throw new Error(`Token-bytet misslyckades: ${await res.text()}`);
  return (await res.json()) as {
    access_token: string;
    refresh_token: string;
    expires_in: number;
    scope: string;
  };
}

Vanliga fel

FelkodOrsak
unauthorized_clientOkänd client_id — kontrollera att appen är registrerad och aktiv.
invalid_request (redirect_uri)redirect_uri matchar ingen av appens registrerade adresser.
invalid_request (code_challenge)PKCE saknas eller är för kort.
unsupported_response_typeEndast response_type=code stöds.
invalid_scopeDu begär en behörighet appen inte har tilldelats.
access_deniedAnvändaren avbröt godkännandet.
invalid_clientFel eller saknad client_secret.
invalid_grantKoden är använd, utgången, fel redirect_uri, misslyckad PKCE eller återkallad refresh_token.
unsupported_grant_typeEndast authorization_code och refresh_token stöds.

Auktoriseringsadressen är rate limitad till 60 anrop/minut per IP och token-endpointen till 20 anrop/minut per IP.

Behöver ni hjälp med er integration?

Vi hjälper er igång med OAuth-kopplingen och rätt behörigheter för ert flöde.