Quick start
Base URL: https://tidvis.se/api/public/v1. All calls use JSON. The Public API is part of the Enterprise plan.
- 1. Create an API client under Sign → Settings → API and save your
client_idandclient_secret. - 2. Exchange them for an
access_tokenvia OAuth2 client credentials. - 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_urlis set) so admins can jump back to the CRM record. - Pass
senderin the request and the sender is auto-signed onsend— 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.
/oauth/access-tokenIssue an access token (1h TTL).
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"expires_at": "2026-06-07T13:00:00.000Z"
}/oauth/access-tokenRevoke the current token (requires Authorization header).
Agreements
/agreementsCreate 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}'/agreementsList agreements. Supports ?status, ?limit, ?cursor.
/agreements/:idFetch an agreement with participants and status. Also the status-polling endpoint — see below.
/agreements/:id/eventsAudit trail (sent, viewed, signed, completed...).
/agreements/:id/downloadFetch 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
/agreements/:id/participantsAdd 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
}/agreements/:id/participants/:pid/declineDecline the agreement for a specific participant. Sets agreement status to 'declined'.
{ "reason": "Terms not acceptable" }/agreements/:id/participants/:pid/delegateDelegate 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."
}/agreements/:id/participants/:pid/notificationsEnable or disable reminder emails for a single participant.
{ "enabled": false }/agreements/:id/chatList chat messages exchanged between the agent and participants on an agreement.
/agreements/:id/chatPost 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
/agreements/:id/documents/mainUpload 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.pdfLifecycle
/agreements/:id/lifecycleSend 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/lifecyclewithaction: "send"returns402 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/lifecyclewithaction: "send"returns402 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 templatestemplates:manage— create and delete templatesgenerations:create— generate new agreements from a templategenerations:read— list, fetch and download generated agreements- If
signature_method: "bankid"is used ingenerate,agreements:sendis also required.
/agreement-templatesList saved agreement templates.
/agreement-templatesCreate 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"/agreement-templates/:idFetch a template schema (fields, blocks, sources).
/agreement-templates/:idPermanently delete a template.
/agreement-templates/:id/generateGenerate 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" }
]
}/agreement-generations?template_id=...List generated agreements (optionally filter by template_id).
/agreement-generations/:idFetch metadata for a generation.
/agreement-generations/:id/downloadSigned 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.
/webhooksRegister a webhook.
{
"client_id": "<api_client uuid>",
"url": "https://your-app.com/webhooks/tidvis",
"events": ["agreement.sent","agreement.signed","agreement.completed"]
}/webhooksList registered webhooks.
/webhooks/:idRemove a webhook.
Events
agreement.sentagreement.viewedagreement.signedagreement.completedagreement.cancelledagreement.declinedagreement.delegatedparticipant.notifications_changedagreement.chat_messageagreement.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
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed input. |
| 401 | unauthorized | Missing or invalid token. |
| 402 | plan_required | Account lacks the Enterprise plan. Includes upgrade_url. |
| 402 | bankid_disabled | Agreement has bankid_required: true but the account lacks the BankID add-on. |
| 403 | forbidden | No permission for the resource. |
| 404 | not_found | Resource not found. |
| 409 | conflict | Invalid state transition (e.g. sending an already sent agreement). |
| 429 | rate_limited | Too many requests. |
| 500 | server_error | Server 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-streamScopes
API tokens can be restricted to specific scopes. MCP access requires mcp:connect plus any combination of:
agreements:read– fetch agreements & eventsagreements:create– create drafts & participantsagreements:send– send agreements for signingagreements:cancel– cancel agreementsbilling:checkout– generate checkout linktemplates:read/templates:manage– manage agreement templatesgenerations:create/generations:read– generate & read agreements from a templatemcp:connect– required for MCP access
Available tools
tidvis_sign_create_agreementtidvis_sign_add_participanttidvis_sign_upload_pdftidvis_sign_send_agreementtidvis_sign_cancel_agreementtidvis_sign_get_agreementtidvis_sign_list_eventstidvis_sign_create_checkouttidvis_sign_decline_agreementtidvis_sign_delegate_signingtidvis_sign_set_participant_notificationstidvis_sign_list_chat_messagestidvis_sign_post_chat_messagetidvis_sign_list_agreement_templatestidvis_sign_get_agreement_templatetidvis_sign_create_agreement_templatetidvis_sign_generate_agreementtidvis_sign_list_agreement_generationstidvis_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_checkoutwithproduct: "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 whensend_agreementruns 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.