Tidvis
Developers · Enterprise

Tidvis Sign Public API

REST API v1 and webhooks for building your own integrations with Tidvis Sign. OAuth2 client credentials, signed webhooks and the full agreement lifecycle.

Quick start

Base URL: https://tidvis.se/api/public/v1. All calls use JSON. The Public API is part of the Enterprise plan.

  1. 1. Create an API client under Sign → Settings → API and save your client_id and client_secret.
  2. 2. Exchange them for an access_token via OAuth2 client credentials.
  3. 3. Create an agreement, upload a PDF, add participants and send.
curl -X POST https://tidvis.se/api/public/v1/oauth/access-token \
  -H "Content-Type: application/json" \
  -d '{"client_id":"tvc_...","client_secret":"tvs_..."}'

One unified model — API and platform share the same agreement

Agreements created via the API live in the same table and share the entire infrastructure with agreements created in the web UI:

  • Visible in the platform under the Via integration tab in /app/sign/dokument.
  • Can be opened, patched, reminded and cancelled from both API and UI.
  • Same auto-reminder cron, archive and 18-month retention as native agreements.
  • Participant signing URL is always https://tidvis.se/sign/<token> — same flow as agreements created in the platform.
  • UI shows a Via {external_source} badge (clickable when external_url is set) so admins can jump back to the CRM record.
  • Pass sender in the request and the sender is auto-signed on send — same behavior as the platform UI.

Authentication

OAuth2 client credentials. Exchange client_id+client_secret for a JWT valid for 1 hour. Send Authorization: Bearer <access_token> on every subsequent call.

POST/oauth/access-token

Issue an access token (1h TTL).

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "expires_at": "2026-06-07T13:00:00.000Z"
}
DELETE/oauth/access-token

Revoke the current token (requires Authorization header).

Agreements

POST/agreements

Create a draft agreement (status: draft).

curl -X POST https://tidvis.se/api/public/v1/agreements \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Employment contract Anna","expires_in_days":14,"bankid_required":true}'
GET/agreements

List agreements. Supports ?status, ?limit, ?cursor.

GET/agreements/:id

Fetch an agreement with participants and status. Also the status-polling endpoint — see below.

GET/agreements/:id/events

Audit trail (sent, viewed, signed, completed...).

GET/agreements/:id/download

Fetch the signed PDF (returns a signed URL or binary content).

Poll signing status

Tidvis Sign has no separate /status resource — GET /agreements/:id is the polling endpoint. The response includes the agreement status and participants[].status + signed_at per party, so you can render "1 of 2 signed" in your own UI without waiting for a webhook.

Recommendation: use webhooks (agreement.viewed, agreement.signed, agreement.completed) as the primary channel and fall back to polling at a 30 s interval when webhooks aren't available. Agreement status values: draft, sent, partially_signed, completed, declined, cancelled.

# Poll status and count signers
curl -s https://tidvis.se/api/public/v1/agreements/$ID \
  -H "Authorization: Bearer $TOKEN" \
  | jq '{
      status,
      signed: ([.participants[] | select(.status == "signed")] | length),
      total: (.participants | length),
      participants: [.participants[] | {name, email, status, signed_at}]
    }'

Sample response (1 of 2 signed):

{
  "status": "partially_signed",
  "signed": 1,
  "total": 2,
  "participants": [
    { "name": "Anna Andersson", "email": "anna@example.com", "status": "signed",  "signed_at": "2026-06-17T09:14:22Z" },
    { "name": "Erik Eriksson",  "email": "erik@example.com",  "status": "viewed",  "signed_at": null }
  ]
}

Participants

POST/agreements/:id/participants

Add a signer (only in draft state).

{
  "name": "Anna Andersson",
  "email": "anna@example.com",
  "role": "signer",
  "job_title": "CEO",
  "phone": "+46701234567",
  "personal_id_masked": "19800101-XXXX",
  "notifications_enabled": true
}
POST/agreements/:id/participants/:pid/decline

Decline the agreement for a specific participant. Sets agreement status to 'declined'.

{ "reason": "Terms not acceptable" }
POST/agreements/:id/participants/:pid/delegate

Delegate signing to a new person. The original participant is marked 'delegated'.

{
  "name": "Erik Eriksson",
  "email": "erik@example.com",
  "job_title": "CFO",
  "phone": "+46707654321",
  "message": "Forwarding to Erik who handles signing."
}
PATCH/agreements/:id/participants/:pid/notifications

Enable or disable reminder emails for a single participant.

{ "enabled": false }
GET/agreements/:id/chat

List chat messages exchanged between the agent and participants on an agreement.

POST/agreements/:id/chat

Post a chat message. Pass participant_id to relay a message from a participant. Fires the agreement.chat_message webhook.

{
  "body": "Hi, could you review appendix 2?",
  "sender_label": "Tidvis Sign Agent",
  "participant_id": "8a3..."
}

PDF documents

PUT/agreements/:id/documents/main

Upload the main document. Accepts application/pdf (binary) or JSON with base64.

# JSON / base64
curl -X PUT https://tidvis.se/api/public/v1/agreements/$ID/documents/main \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filename":"agreement.pdf","content_base64":"JVBERi0..."}'

# Or binary
curl -X PUT https://tidvis.se/api/public/v1/agreements/$ID/documents/main \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/pdf" \
  --data-binary @agreement.pdf

Lifecycle

POST/agreements/:id/lifecycle

Send or cancel an agreement.

{"action": "send"}   // or "cancel"

Email OTP (default method)

Every agreement is signed by default using email + one-time code (OTP). No extra configuration required – when bankid_required is false or omitted, each participant uses this method. Counts as a simple electronic signature (SES) under eIDAS.

How it works

  • The recipient receives a personal link to their email (the address in participants[].email).
  • A 6-digit one-time code is sent on document open, valid for 15 minutes, max 5 attempts.
  • Identity is verified before the signature is recorded; IP, user agent, timestamp and code verification are stored in the audit trail.
  • Included on every plan – no add-on or per-signature fee.

Create an OTP agreement (default)

curl -X POST https://tidvis.se/api/public/v1/agreements \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Consulting agreement Anna",
    "expires_in_days": 14
  }'

Webhook payload for OTP signatures

{
  "event": "agreement.signed",
  "agreement_id": "agr_...",
  "participant": { "id": "p_...", "email": "anna@example.com" },
  "signature": {
    "method": "otp_email",
    "signer_name": "Anna Andersson",
    "email": "anna@example.com",
    "code_verified_at": "2026-06-11T08:41:53.000Z",
    "signed_at": "2026-06-11T08:42:11.000Z",
    "ip": "203.0.113.42",
    "user_agent": "Mozilla/5.0 ..."
  }
}

BankID

Alternative to the default Email OTP method when you need advanced electronic signatures (AES). Set bankid_required: true on creation (or via PATCH before sending). When the agreement is sent, each participant signs with BankID on mobile or desktop instead of entering a one-time code.

Requirements

  • The account must have the Sign Pro plan and the BankID add-on enabled.
  • Pricing: 100 BankID signatures included per month, then 5 SEK/signature.
  • Without the add-on, POST /agreements/:id/lifecycle with action: "send" returns 402 bankid_disabled.

Create a BankID agreement

curl -X POST https://tidvis.se/api/public/v1/agreements \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Employment contract Anna",
    "bankid_required": true
  }'

Response (abbreviated):

{
  "id": "agr_...",
  "status": "draft",
  "bankid_required": true,
  "document": { "uploaded": false, "sha256": null },
  "participants": []
}

Webhook payload when BankID is used

For agreements with bankid_required: true, the agreement.signed event is enriched with a signature block. For privacy reasons, neither personal identity numbers, certificates nor OCSP responses are exposed via the API or webhooks.

{
  "event": "agreement.signed",
  "agreement_id": "agr_...",
  "participant": { "id": "p_...", "email": "anna@example.com" },
  "signature": {
    "method": "bankid",
    "signer_name": "Anna Andersson",
    "signed_at": "2026-06-11T08:42:11.000Z"
  }
}

The signature.method field is always one of "otp_email" or "bankid". Agreements with mixed participants log the method per signer.

BankID identification before opening

Optional gate that forces the recipient to identify with BankID before the document is shown. It is independent of the chosen signature method — you can require BankID identification and still let the recipient sign with Email OTP, click-to-sign, or drawn signature. Combine with bankid_required: true when you want both identification before opening and BankID for the actual signature.

Enable on creation

curl -X POST https://tidvis.se/api/public/v1/agreements \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "NDA Anna",
    "bankid_identify_required": true
  }'

Response (abbreviated):

{
  "id": "agr_...",
  "status": "draft",
  "bankid_required": false,
  "bankid_identify_required": true,
  "participants": []
}

Recipient behavior

  • When the recipient opens the link, a BankID gate is shown instead of the PDF.
  • They must start BankID on mobile or via QR code and confirm the identification.
  • Only then is the PDF rendered and the chosen signature method enabled.
  • The "viewed" event is logged after identification, not on the first click.

Requirements and pricing

  • Account must have Sign Pro with the BankID add-on — same prerequisites as bankid_required.
  • Identifications do not count against the 100 BankID signatures/month quota. Only actual BankID signatures (bankid_required: true) are billed at 5 SEK each beyond the included amount.
  • Without the add-on, POST /agreements/:id/lifecycle with action: "send" returns 402 bankid_disabled.

Webhook payload

A dedicated agreement.identified event is delivered when a recipient identifies. Personal identity numbers, certificates and OCSP responses are never exposed via the API.

{
  "event": "agreement.identified",
  "agreement_id": "agr_...",
  "participant": { "id": "p_...", "email": "anna@example.com" },
  "identification": {
    "method": "bankid",
    "signer_name": "Anna Andersson",
    "identified_at": "2026-06-12T10:21:04.000Z"
  }
}

Agreement generator

Build an AI-driven agreement template from 2–10 example contracts and then generate new agreements with a single call. Generated agreements can be downloaded as DOCX or sent straight to signing via Sign — in which case you get back a sign_document_id.

Requirements & pricing

  • The agreement module must be active (99 SEK/user/month). Otherwise the endpoint returns 402 plan_required.
  • MCP/agent calls add 3 SEK per generation on top of the module fee.
  • Source files max 20 MB each, 2–10 files per template (PDF or DOCX).

Scopes

  • templates:read — list and fetch templates
  • templates:manage — create and delete templates
  • generations:create — generate new agreements from a template
  • generations:read — list, fetch and download generated agreements
  • If signature_method: "bankid" is used in generate, agreements:send is also required.
GET/agreement-templates

List saved agreement templates.

POST/agreement-templates

Create a template from 2–10 example agreements. Multipart (files[]) or JSON with base64 (sources[]).

curl -X POST https://tidvis.se/api/public/v1/agreement-templates \
  -H "Authorization: Bearer $TOKEN" \
  -F "name=Consulting agreement" \
  -F "files=@sample1.pdf" \
  -F "files=@sample2.pdf" \
  -F "files=@sample3.docx"
GET/agreement-templates/:id

Fetch a template schema (fields, blocks, sources).

DELETE/agreement-templates/:id

Permanently delete a template.

POST/agreement-templates/:id/generate

Generate a new agreement from a template. Returns DOCX inline (default) or a sent Sign draft when send_for_signing=true and recipients are provided.

{
  "values": {
    "customer_name": "Acme Ltd",
    "amount": 45000,
    "start_date": "2026-08-01"
  },
  "send_for_signing": true,
  "signature_method": "bankid",
  "recipients": [
    { "name": "Anna Andersson", "email": "anna@acme.com" }
  ]
}
GET/agreement-generations?template_id=...

List generated agreements (optionally filter by template_id).

GET/agreement-generations/:id

Fetch metadata for a generation.

GET/agreement-generations/:id/download

Signed download URL (10-minute TTL) for the generated PDF/DOCX.

Full request/response schemas live in the OpenAPI reference.

Webhooks

Tidvis delivers events to your URL via POST JSON. Every request is HMAC-SHA256 signed in the header X-Tidvis-Signature: sha256=<hex> based on your signing_secret and the raw body. Failed deliveries are retried with exponential backoff for up to 24h.

POST/webhooks

Register a webhook.

{
  "client_id": "<api_client uuid>",
  "url": "https://your-app.com/webhooks/tidvis",
  "events": ["agreement.sent","agreement.signed","agreement.completed"]
}
GET/webhooks

List registered webhooks.

DELETE/webhooks/:id

Remove a webhook.

Events

  • agreement.sent
  • agreement.viewed
  • agreement.signed
  • agreement.completed
  • agreement.cancelled
  • agreement.declined
  • agreement.delegated
  • participant.notifications_changed
  • agreement.chat_message
  • agreement.expired

For agreements with bankid_required: true, the agreement.signed/agreement.completed events include a signature block. See BankID.

Verify signature (Node.js)

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const given = (header || "").replace(/^sha256=/, "");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}

Error codes

HTTPCodeMeaning
400invalid_requestMalformed input.
401unauthorizedMissing or invalid token.
402plan_requiredAccount lacks the Enterprise plan. Includes upgrade_url.
402bankid_disabledAgreement has bankid_required: true but the account lacks the BankID add-on.
403forbiddenNo permission for the resource.
404not_foundResource not found.
409conflictInvalid state transition (e.g. sending an already sent agreement).
429rate_limitedToo many requests.
500server_errorServer error – retry.

Rate limits

Default: 60 requests/minute per client and 600/hour for uploads. When exceeded you'll get 429 rate_limited with a Retry-After header. Need more? Contact us.

MCP & AI agents

Tidvis Sign exposes a Model Context Protocol server so AI agents (ChatGPT, Claude Desktop, custom agents) can create, send and track agreements with a single tool call. Discovery manifest at /.well-known/mcp.json. A dedicated landing page lives at /en/developers/mcp.

Endpoint

POST https://tidvis.se/api/mcp
Authorization: Bearer <access_token>
Content-Type: application/json
Accept: application/json, text/event-stream

Scopes

API tokens can be restricted to specific scopes. MCP access requires mcp:connect plus any combination of:

  • agreements:read – fetch agreements & events
  • agreements:create – create drafts & participants
  • agreements:send – send agreements for signing
  • agreements:cancel – cancel agreements
  • billing:checkout – generate checkout link
  • templates:read / templates:manage – manage agreement templates
  • generations:create / generations:read – generate & read agreements from a template
  • mcp:connect – required for MCP access

Available tools

  • tidvis_sign_create_agreement
  • tidvis_sign_add_participant
  • tidvis_sign_upload_pdf
  • tidvis_sign_send_agreement
  • tidvis_sign_cancel_agreement
  • tidvis_sign_get_agreement
  • tidvis_sign_list_events
  • tidvis_sign_create_checkout
  • tidvis_sign_decline_agreement
  • tidvis_sign_delegate_signing
  • tidvis_sign_set_participant_notifications
  • tidvis_sign_list_chat_messages
  • tidvis_sign_post_chat_message
  • tidvis_sign_list_agreement_templates
  • tidvis_sign_get_agreement_template
  • tidvis_sign_create_agreement_template
  • tidvis_sign_generate_agreement
  • tidvis_sign_list_agreement_generations
  • tidvis_sign_get_agreement_generation

Claude Desktop configuration

{
  "mcpServers": {
    "tidvis-sign": {
      "url": "https://tidvis.se/api/mcp",
      "headers": { "Authorization": "Bearer YOUR_ACCESS_TOKEN" }
    }
  }
}

Agent checkout & pay-per-agreement

Two ways for an agent to let the end customer pay:

  • Sign Pro (subscription) – monthly fee per seat, unlimited agreements. Use tidvis_sign_create_checkout with product: "sign_pro_monthly" and the desired number of seats.
  • Pay-per-agreement – 19 SEK/agreement as prepaid credits, ideal for low-volume agents. Use product: "sign_per_agreement" and a credit count (1–500). Credits are consumed automatically when send_agreement runs on free-plan accounts.

The checkout tool returns a Stripe-hosted URL the agent shows to the end customer. A 402 plan_required on send_agreement means the account has neither credits nor Sign Pro – the response includes an upgrade_url the agent can link to.

Ready to see Tidvis?

Book a no-obligation demo. We'll show you the system tailored to your operation, with no sales pressure.