# NextSign API Documentation Full Context
> Expanded machine-readable reference for the NextSign API docs. This derivative file is published for tooling that prefers a single long context file.
Primary docs index: https://docs.nextsign.dk/docs
Short overview file: https://docs.nextsign.dk/llms.txt
OpenAPI contract: https://docs.nextsign.dk/openapi.json
Important notes:
- This file is generated from the published documentation pages in the repository.
- It preserves examples and code blocks, but removes frontmatter and import boilerplate.
- The canonical human-facing pages remain the source of truth.
## Contents
- [Introduction](https://docs.nextsign.dk/docs): Complete API documentation for automating forms, cases, document signing, and file retrieval using NextSign.
- [Authorization](https://docs.nextsign.dk/docs/authorization): Find your API key and companyId, and authenticate requests to the NextSign API.
- [Test Environment](https://docs.nextsign.dk/docs/testenv): Build and validate your NextSign integration safely without affecting production data.
- [API Versioning](https://docs.nextsign.dk/docs/versioning): Understand how NextSign versions endpoints and how to safely work with both v2 and v3 routes.
- [Rate Limits](https://docs.nextsign.dk/docs/rate-limits): Understand the current NextSign API request limit and how to build retry-safe clients around it.
- [Idempotency](https://docs.nextsign.dk/docs/idempotency): Understand the difference between request idempotency, resource IDs, and webhook deduplication in NextSign.
- [Template Tags](https://docs.nextsign.dk/docs/template-tags): The {tag} placeholder syntax used in .docx templates — values, conditions, lists, empty-state companions, dropdown options, recipient tags, global fields and the company logo tag.
- [Objects](https://docs.nextsign.dk/docs/objects): Shared request and response objects used across cases, forms, and document endpoints.
- [Forms Get](https://docs.nextsign.dk/docs/forms-get): Get a form by ID using GET /api/v2/{company}/forms/{form_id}/get.
- [Forms Tags](https://docs.nextsign.dk/docs/forms-tags): Get form tags using GET /api/v2/{company}/forms/{form_id}/tags/get.
- [Forms Submit](https://docs.nextsign.dk/docs/forms-submit): Submit a form using POST /api/v2/{company}/forms/{form_id}/submit.
- [Case List](https://docs.nextsign.dk/docs/cases): List cases with filters using GET /api/v2/{company}/cases/get.
- [Case Get Multiple](https://docs.nextsign.dk/docs/cases-get-multiple): Get multiple cases by ID using GET /api/v2/{company}/cases/{case_ids}/get.
- [Case Get](https://docs.nextsign.dk/docs/case): Get a single case by ID using GET /api/v2/{company}/case/{case_id}/get.
- [Case Create](https://docs.nextsign.dk/docs/case-create): Create a case using POST /api/v2/{company}/case/create.
- [Case Update](https://docs.nextsign.dk/docs/case-update): Update an existing case using PATCH /api/v2/{company}/case/{case_id}/update.
- [Case Delete](https://docs.nextsign.dk/docs/case-delete): Delete a case using GET /api/v2/{company}/case/{case_id}/delete.
- [Document Upload](https://docs.nextsign.dk/docs/documents-upload): Upload a file using POST /v3/company/{company}/file/upload.
- [Document Retrieve](https://docs.nextsign.dk/docs/documents): Retrieve a pre-signed document URL using POST /v3/company/{company}/file/view-presigned-url.
- [Document Convert](https://docs.nextsign.dk/docs/documents-convert): Convert documents using POST /api/v2/document/convert.
- [Webhooks](https://docs.nextsign.dk/docs/webhooks): Receive real-time notifications when case and flow events occur in NextSign.
- [Webhook Retry Logic](https://docs.nextsign.dk/docs/webhook-retry-logic): Understand how NextSign retries failed webhook deliveries and how to build duplicate-safe handlers.
- [Overview](https://docs.nextsign.dk/docs/v3): Create and send documents for signing from your own system, with the NextSign API.
- [Authorization](https://docs.nextsign.dk/docs/v3/authorization): Create an API key, send it with every request, and understand why some keys are rejected.
- [Create a Case](https://docs.nextsign.dk/docs/v3/case-create): Create and send a signing case with POST /v3/api/cases — full request and response reference.
- [List Cases](https://docs.nextsign.dk/docs/v3/case-list): Page through your company's cases with GET /v3/api/cases — filters, cursor pagination and the response shape.
- [Retrieve a Case](https://docs.nextsign.dk/docs/v3/case-get): Fetch one case by id with GET /v3/api/cases/{id}, including its signing links and document downloads.
- [List Documents](https://docs.nextsign.dk/docs/v3/documents-list): Page through your document library with GET /v3/api/documents — which documents your key can use as the source of a case.
- [Retrieve a Document](https://docs.nextsign.dk/docs/v3/documents-get): Fetch one library document with GET /v3/api/documents/{id} — its versions and, for a .docx, the exact fields you can fill when creating a case.
- [Create a Case from a Document](https://docs.nextsign.dk/docs/v3/documents-create-case): Fill a library document and send it for signing with POST /v3/api/documents/{id}/cases — the document-first way to create a case.
- [Errors](https://docs.nextsign.dk/docs/v3/errors): The v3 error envelope, the full list of error slugs, and what is safe to retry.
- [Overview](https://docs.nextsign.dk/docs/mcp): Let your AI assistant create and send NextSign signing cases for you — connect once, then just ask in plain language.
- [Connect Your Assistant](https://docs.nextsign.dk/docs/mcp/connect): Add the NextSign MCP server to Claude or ChatGPT, sign in, and check that it works.
- [Using It](https://docs.nextsign.dk/docs/mcp/usage): Example prompts, what happens when the assistant creates a case, and habits that keep you safe.
- [create_case](https://docs.nextsign.dk/docs/mcp/create-case): Every field the create_case tool accepts, and what it returns — the same payload as POST /v3/api/cases.
- [list_documents](https://docs.nextsign.dk/docs/mcp/list-documents): The list_documents tool — the documents in your library that the assistant can turn into a signing case.
- [get_document](https://docs.nextsign.dk/docs/mcp/get-document): The get_document tool — one library document, its versions, and the exact fields to fill when creating a case from it.
- [create_case_from_document](https://docs.nextsign.dk/docs/mcp/create-case-from-document): Every field the create_case_from_document tool accepts — fill a library document and send it for signing, the same as POST /v3/api/documents/{id}/cases.
- [Troubleshooting](https://docs.nextsign.dk/docs/mcp/troubleshooting): Common errors when connecting or creating cases, and the current limitations of the beta.
- [Company Status](https://docs.nextsign.dk/docs/company-status): Retrieve your remaining credit balance and credit usage using GET /v3/company/{company}/status.
- [Get a Webflow](https://docs.nextsign.dk/docs/v3/webflows-get): Retrieve webflow progress, recipient links and non-sensitive answers.
- [Migrating from v2](https://docs.nextsign.dk/docs/v3/migration): Field-by-field mapping from POST /api/v2/{company}/case/create to POST /v3/api/cases, and every behavioural difference.
- [Send a Webflow](https://docs.nextsign.dk/docs/v3/webflows-create): Request information using a published webflow and email invitations.
- [White-Label Partner Program](https://docs.nextsign.dk/docs/partner-program): Offer e-signing that looks like your own, on your domain and brand. Refer customers, earn a kickback, and get an API key for every new account.
## Getting Started
Authentication, environments, and the fastest way to make your first request.
### Introduction
Source: https://docs.nextsign.dk/docs
Summary: Complete API documentation for automating forms, cases, document signing, and file retrieval using NextSign.
Welcome to the **NextSign API** documentation.
NextSign is a comprehensive API platform that allows you to **automate document creation, digital signing, case management, and secure document retrieval**. It is designed for developers who want to integrate legally binding electronic signatures and document workflows directly into their applications.
With NextSign, you can generate contracts from templates, populate documents dynamically, send them for signing, track progress, and retrieve signed files — all through a secure and scalable API.
---
## What You Can Build
Using the NextSign API, you can:
- Automate contract creation from reusable templates
- Populate documents dynamically using tags
- Send documents for electronic signing
- Track and manage signing cases
- Retrieve signed documents securely
- Convert DOCX templates to PDF with tag replacement
The API is suitable for CRMs, ERP systems, internal tools, and customer-facing platforms.
---
## Core API Modules
### Forms
Forms allow you to create reusable document templates that can be submitted multiple times with different data. They are ideal for contracts, agreements, and standardized documents.
With the Forms API, you can:
- Retrieve form details and configuration
- Fetch available tags for dynamic data population
- Submit forms to create signing cases
- Control reminders, expiration, access roles, and deletion policies
---
### Cases
Cases represent individual signing processes created from forms or direct submissions.
Using the Cases API, you can:
- Retrieve multiple cases using filters
- Fetch specific cases by ID
- Update case status, settings, recipients, and attached documents
- Monitor signing status and metadata
- Build dashboards and reporting tools
---
### Documents
The Documents API provides secure access to files, uploads, and document conversion features.
You can:
- Upload larger files before case creation
- Generate pre-signed URLs for signed documents
- Retrieve completed contracts securely
- Convert DOCX files to PDF
- Replace tags dynamically before sending or previewing documents
---
## Platform Guides
The platform guides explain how to build integrations that stay reliable in production:
- Use [Authorization](https://docs.nextsign.dk/docs/authorization) to set up API keys and `companyId`
- Use [API Versioning](https://docs.nextsign.dk/docs/versioning) to understand when to call `v2` and when to call `v3`
- Use [Rate Limits](https://docs.nextsign.dk/docs/rate-limits) to design around the current `100 requests per minute` limit
- Use [Idempotency](https://docs.nextsign.dk/docs/idempotency) to understand request deduplication limits and webhook-safe handling
- Use [Webhooks](https://docs.nextsign.dk/docs/webhooks) and [Webhook Retry Logic](https://docs.nextsign.dk/docs/webhook-retry-logic) to handle event delivery safely
- Use [White-Label Partner Program](https://docs.nextsign.dk/docs/partner-program) to offer signing under your own brand, earn a kickback, and receive an API key for every new account
---
## Authentication
All API requests require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token in the `Authorization` header for every request.
```http
Authorization: Bearer YOUR_API_KEY
```
### Creating an API Key
NextSign uses Bearer Token authentication, and your API key is the bearer credential you send with each request.
#### Steps to Create an API Key
1. **Log in to your NextSign Dashboard**
2. Navigate to **Dashboard → Config → API Keys**
3. Click **Create API Key**
4. Enter a name for the key (for example: `Production API Key` or `Backend Service`)
5. Click **Generate**
Once generated, your API key will be displayed only once.
Your `companyId` is also available on the **API Keys** page, shown at the top of the screen.
**Important:** For security reasons, the API key cannot be viewed again after you leave this screen. Make sure to copy and store it securely on your computer or in a password manager before closing the page. If the key is lost, you will need to generate a new API key from the dashboard.
#### Using the API Key
Include the API key in the `Authorization` header of your API requests:
```http
Authorization: Bearer YOUR_API_KEY
```
**Example Request:**
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/cases/get' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```javascript title="JavaScript"
const response = await fetch(
'https://www.nextsign.dk/api/v2/{company}/cases/get',
{
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
}
}
);
```
```python title="Python"
import requests
url = "https://www.nextsign.dk/api/v2/{company}/cases/get"
headers = {
"Authorization": "Bearer YOUR_API_KEY"
}
response = requests.get(url, headers=headers)
```
### Security Best Practices
Following security best practices is essential when working with API keys:
- **Do not expose your API key in client-side code** - API keys should only be used in server-side applications
- **Never commit API keys to version control** - Use environment variables or `.env` files (added to `.gitignore`)
- **Rotate keys periodically** and revoke unused ones from the dashboard
- **Store keys securely** using environment variables or a secrets manager (e.g., AWS Secrets Manager, Azure Key Vault, HashiCorp Vault)
- **Use separate API keys** for development, staging, and production environments
- **Monitor API key usage** regularly to detect any unauthorized access
If you suspect your API key has been compromised, immediately revoke it from the NextSign dashboard and generate a new one.
---
## Getting Started
Ready to integrate NextSign into your application? Here's how to get started:
1. **Generate your API key** from the NextSign dashboard
2. **Choose an API module** - Start with Forms, Cases, or Documents based on your use case
3. **Make your first request** - Test authentication and basic endpoints
4. **Build your integration** - Refer to the detailed endpoint documentation for each module
Explore the documentation sections for detailed information on each API module and endpoint.
---
## Endpoint Reference
### Setup
- [Authorization](https://docs.nextsign.dk/docs/authorization) - API key, bearer token, and companyId setup
- [API Versioning](https://docs.nextsign.dk/docs/versioning) - Understand how `v2` and `v3` endpoints are versioned
- [Rate Limits](https://docs.nextsign.dk/docs/rate-limits) - Plan around the current `100 requests per minute` limit
- [Idempotency](https://docs.nextsign.dk/docs/idempotency) - Build duplicate-safe request and webhook handling
- [Test Environment](https://docs.nextsign.dk/docs/testenv) - Work safely against the test environment
### Reference
- [Objects](https://docs.nextsign.dk/docs/objects) - Shared recipient, document, settings, signing schema, and state definitions
### Forms
- [Forms Get](https://docs.nextsign.dk/docs/forms-get) - Retrieve a form by ID
- [Forms Tags](https://docs.nextsign.dk/docs/forms-tags) - Retrieve available tags for a form
- [Forms Submit](https://docs.nextsign.dk/docs/forms-submit) - Submit a form and create a case
### Cases
- [Case List](https://docs.nextsign.dk/docs/cases) - List cases with filters
- [Case Get Multiple](https://docs.nextsign.dk/docs/cases-get-multiple) - Fetch multiple cases by ID
- [Case Get](https://docs.nextsign.dk/docs/case) - Fetch a single case by ID
- [Case Create](https://docs.nextsign.dk/docs/case-create) - Create a new case
- [Case Update](https://docs.nextsign.dk/docs/case-update) - Update a case after creation
- [Case Delete](https://docs.nextsign.dk/docs/case-delete) - Delete a case by ID
### Documents
- [Document Upload](https://docs.nextsign.dk/docs/documents-upload) - Upload larger files before case creation
- [Document Retrieve](https://docs.nextsign.dk/docs/documents) - Generate a pre-signed URL for a stored document
- [Document Convert](https://docs.nextsign.dk/docs/documents-convert) - Convert DOCX to PDF with tag replacement
### Platform
- [Webhooks](https://docs.nextsign.dk/docs/webhooks) - Receive event notifications
- [Webhook Retry Logic](https://docs.nextsign.dk/docs/webhook-retry-logic) - Understand retry timing and duplicate-safe consumers
### Authorization
Source: https://docs.nextsign.dk/docs/authorization
Summary: Find your API key and companyId, and authenticate requests to the NextSign API.
NextSign API requests use your **API key** as a bearer token and your **companyId** as part of the request URL.
## 1. Create an API Key
Create API keys from the NextSign dashboard:
1. Open **Dashboard → Config → API Keys**
2. Click **Create API Key**
3. Enter a name such as `Backend Service` or `Test Integration`
4. Click **Generate**
5. Copy the key and store it securely
Your API key is shown only once. If you lose it, create a new one and revoke the old key.
## 2. Use the API Key as Bearer Token
Send the API key in the `Authorization` header:
```http
Authorization: Bearer YOUR_API_KEY
```
Example:
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/cases/get' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
## 3. Find Your companyId
Your `companyId` is the company identifier used in API paths such as:
```text
https://www.nextsign.dk/api/v2/{company}/cases/get
```
The easiest places to find it:
- At the top of **Dashboard → Config → API Keys**
- In many dashboard URLs, where it appears as the `/user/{companyId}/...` path segment
- In existing API requests or integration config where the company identifier is already stored
Recommended approach:
1. Open **Dashboard → Config → API Keys**
2. Copy the `companyId` shown at the top of the page
3. Use the URL only as a fallback if needed
Example dashboard-style URL:
```text
https://www.nextsign.dk/user/6576e19662ec13c4faa7de7b/settings
```
In this example, `6576e19662ec13c4faa7de7b` is the `companyId`.
## 4. Recommended Environment Variables
```bash title=".env"
NEXTSIGN_API_KEY=your_api_key_here
NEXTSIGN_COMPANY_ID=your_company_id_here
NEXTSIGN_API_URL=https://www.nextsign.dk/api
```
## 5. First Authenticated Request
```bash title="cURL"
curl --location "https://www.nextsign.dk/api/v2/${NEXTSIGN_COMPANY_ID}/cases/get" \
-H "Authorization: Bearer ${NEXTSIGN_API_KEY}"
```
## Typical Error Responses
Missing bearer token:
```json title="Unauthorized"
{
"errors": [
{
"code": 401,
"message": "Unauthorized",
"description": "Bearer token not found"
}
]
}
```
Invalid company id:
```json title="Bad Request"
{
"code": 400,
"message": "Bad Request",
"description": "Invalid company id"
}
```
Some older v2 endpoints return validation or authorization problems inside a `200 OK` response with an `errors` array. In integrations, validate both the HTTP status code and the response body.
### Test Environment
Source: https://docs.nextsign.dk/docs/testenv
Summary: Build and validate your NextSign integration safely without affecting production data.
NextSign provides a **Test Environment** that is a full replica of the production environment. It allows you to safely build, test, and validate your integration without affecting live data or real signing workflows.
The test environment mirrors production behavior, APIs, and workflows, making it the perfect sandbox for development and quality assurance.
---
## Use Cases
The test environment is ideal for:
- **API integration testing** - Test all API endpoints and responses
- **Webhook testing** - Validate webhook payloads and event handling
- **Document generation and signing flows** - Test form submissions and case creation
- **End-to-end workflow validation** - Verify complete signing workflows
- **Development and QA** - Safe environment for developers and QA teams
- **Staging deployments** - Pre-production testing before going live
---
## Test Environment URL
Access the test environment at:
```text
https://test.nextsign.dk
```
Test environment users are separate from production. You must create a new user on [https://test.nextsign.dk](https://test.nextsign.dk) to access the test environment, and all data and configurations are kept completely separate.
---
## Key Characteristics
The test environment provides:
| Feature | Description |
| ------- | ----------- |
| **Same API structure** | Identical endpoints and request/response formats as production |
| **Same authentication** | Uses Bearer Token authentication, configured the same way |
| **Same webhook flow** | Full webhook configuration and delivery system |
| **Isolated data** | No impact on production data or workflows |
| **Safe testing** | Perfect for development, QA, and staging purposes |
| **Full feature parity** | All production features available for testing |
Data created in the test environment is completely isolated from production and may be periodically cleared. Do not rely on test data for long-term storage.
---
## API Keys in Test Environment
API keys are environment-specific and **cannot be shared between test and production**.
### Key Separation
You must generate separate API keys for:
- ✅ **Test environment** - For development and testing
- ✅ **Production environment** - For live applications
API keys created in the test environment are NOT valid in production, and vice versa. Attempting to use a production API key in the test environment will result in authentication errors.
### Creating Test API Keys
Generate test API keys from the test environment dashboard:
1. Create a new user on [https://test.nextsign.dk](https://test.nextsign.dk) and log in with that test account
2. Navigate to **Dashboard → Config → API Keys**
3. Click **Create API Key**
4. Enter a descriptive name (e.g., `Development API Key`)
5. Click **Generate**
6. Copy and securely store the API key
**Example test API key usage:**
```bash title="cURL - Test Environment"
curl --location 'https://test.nextsign.dk/api/v2/{company}/cases/get' \
-H 'Authorization: Bearer TEST_API_KEY_HERE'
```
---
## Webhooks in Test Environment
Webhooks configured in the test environment are also isolated from production.
### Test Webhook Behavior
Webhooks in the test environment:
- ✅ Trigger only for test cases and test workflows
- ✅ Must point to a publicly accessible test or staging endpoint
- ✅ Are ideal for validating webhook payloads and event handling
- ✅ Use the same event types and payload structure as production
### Recommended Webhook Setup
For testing webhooks, you can:
1. **Use a staging server** - Point to your staging/development backend
2. **Use webhook testing tools** - Services like [webhook.site](https://webhook.site) or [RequestBin](https://requestbin.com)
3. **Use ngrok for local development** - Tunnel your local server for testing
**Example test webhook configuration:**
```text
URL: https://staging.yourapp.com/webhooks
Headers:
X-Signature: test-secret-key
Triggers:
☑ Case created
☑ Case signed
☑ Case denied
```
Test webhooks will only receive events from test cases. This ensures complete isolation between test and production environments.
---
## Testing Best Practices
### 1. Use Environment Variables
Keep your API keys and configuration separate using environment variables:
```javascript title=".env.development"
NEXTSIGN_API_URL=https://test.nextsign.dk/api
NEXTSIGN_API_KEY=your_test_api_key_here
NEXTSIGN_COMPANY_ID=your_test_company_id
```
```javascript title=".env.production"
NEXTSIGN_API_URL=https://www.nextsign.dk/api
NEXTSIGN_API_KEY=your_production_api_key_here
NEXTSIGN_COMPANY_ID=your_production_company_id
```
### 2. Test All Event Types
Ensure you test all webhook event types in the test environment:
- Case created
- Recipient signed
- Case signed
- Case denied
- Workflow completed
### 3. Validate Complete Workflows
Test the entire signing workflow from start to finish:
1. Create a form or submit a case
2. Send for signing
3. Complete the signing process
4. Verify webhook delivery
5. Retrieve signed documents
### 4. Load Testing
Use the test environment for load testing to understand API rate limits and performance characteristics.
### 5. Error Handling
Test error scenarios:
- Invalid API keys
- Missing required fields
- Malformed requests
- Webhook failures and retries
---
## Differences from Production
While the test environment mirrors production, be aware of these differences:
| Aspect | Test Environment | Production Environment |
| ------ | ---------------- | ---------------------- |
| **Data persistence** | May be periodically cleared | Permanent storage |
| **Email delivery** | May go to test email addresses | Real recipient emails |
| **API keys** | Separate test keys required | Separate production keys required |
| **Webhooks** | Point to staging/test endpoints | Point to production endpoints |
| **Domain** | test.nextsign.dk | nextsign.dk |
| **Performance** | May have lower rate limits | Production rate limits |
Email notifications in the test environment may be sent to test addresses or suppressed entirely to prevent accidental communication with real recipients.
---
## Going Live
Once your integration has been fully tested in the test environment, follow these steps to deploy to production:
### Pre-Launch Checklist
- [ ] All API integrations tested and working in test environment
- [ ] Webhook endpoints tested and validated
- [ ] Complete signing workflows tested end-to-end
- [ ] Error handling and edge cases covered
- [ ] Security best practices implemented
- [ ] Documentation and runbooks prepared
### Migration Steps
1. **Switch to the production dashboard**
- Log in to [https://nextsign.dk](https://nextsign.dk)
2. **Generate production API keys**
- Navigate to **Dashboard → Config → API Keys**
- Create new API keys for production use
- Store them securely in your production secrets manager
3. **Update your application configuration**
- Replace test API keys with production keys
- Update environment variables
- Change API base URLs if different
- Update company identifiers
4. **Recreate webhooks in production**
- Configure the same webhook triggers as test
- Point to production endpoints (not staging)
- Use production secret keys for signature validation
- Test webhook delivery with a real case
5. **Verify production setup**
- Test a complete workflow in production
- Verify webhook delivery
- Check document retrieval
- Monitor for errors
### Example Environment Switch
```javascript title="config.js"
const config = {
test: {
apiUrl: 'https://test.nextsign.dk/api',
apiKey: process.env.NEXTSIGN_TEST_API_KEY,
companyId: 'test-company-id'
},
production: {
apiUrl: 'https://www.nextsign.dk/api',
apiKey: process.env.NEXTSIGN_PROD_API_KEY,
companyId: 'prod-company-id'
}
};
const env = process.env.NODE_ENV || 'test';
module.exports = config[env];
```
Never use test API keys in production or production API keys in test. Always maintain strict separation between environments.
---
## Monitoring and Debugging
### Test Environment Logs
Monitor your test integration using:
- **Dashboard logs** - View case creation and signing activity
- **Webhook logs** - Check webhook delivery status and payloads
- **API response codes** - Validate request/response patterns
### Common Issues
#### Authentication Errors
- Verify you're using test API keys in the test environment
- Check that API keys are correctly configured in headers
#### Webhook Not Triggering
- Ensure webhooks are enabled (Active toggle is ON)
- Verify the webhook URL is publicly accessible
- Check webhook logs in the dashboard for delivery errors
#### Rate Limiting
- Test environment may have lower rate limits than production
- Design your client around the documented `100 requests per minute` limit
- Implement retry logic with exponential backoff
---
## Support
If you encounter issues while testing:
1. Check the **dashboard logs** for error messages
2. Review the **webhook delivery logs** for webhook issues
3. Verify your **API key configuration**
4. Consult the **API documentation** for endpoint requirements
5. Contact NextSign support with specific error details
When reporting issues, always specify whether the problem occurs in the test environment, production environment, or both.
## Platform
Versioning, rate limits, and idempotency guidance for reliable integrations.
### API Versioning
Source: https://docs.nextsign.dk/docs/versioning
Summary: Understand how NextSign versions endpoints and how to safely work with both v2 and v3 routes.
NextSign versions its API in the request path. Integrate against the exact version documented for each endpoint instead of assuming all resources exist in the same version.
## Current Versioning Pattern
The current documentation uses URL-based versioning:
```text
https://www.nextsign.dk/api/v2/...
https://api.nextsign.dk/v3/...
```
Examples from the current API surface:
- Cases and forms are primarily documented under `v2`
- Newer file endpoints such as upload and view-presigned-url are documented under `v3`
Do not try to upgrade an endpoint by changing only the version segment in the URL. Treat each documented path as its own contract.
## What This Means for Integrations
- Pin the full endpoint URL in your client code
- Keep tests for each version you depend on
- Expect request and response behavior to differ between older and newer endpoints
- Validate both HTTP status codes and response bodies on older `v2` endpoints
## Recommended Strategy
When building or updating an integration:
1. Use the exact endpoint path from the docs
2. Keep `v2` and `v3` routes separate in code
3. Test version changes in the test environment before switching production traffic
4. Review request examples and error handling for the specific endpoint you call
## Common Mistakes to Avoid
- Assuming every file endpoint is under `v2`
- Assuming every newer endpoint also has a `v2` equivalent
- Using a single global version variable for all NextSign requests
- Skipping regression tests when moving from one versioned route to another
## Commonly Used Examples
- [Case Create](https://docs.nextsign.dk/docs/case-create) uses `POST /api/v2/{company}/case/create`
- [Forms Submit](https://docs.nextsign.dk/docs/forms-submit) uses `POST /api/v2/{company}/forms/{form_id}/submit`
- [Document Upload](https://docs.nextsign.dk/docs/documents-upload) uses `POST /v3/company/{company}/file/upload`
- [Document Retrieve](https://docs.nextsign.dk/docs/documents) uses `POST /v3/company/{company}/file/view-presigned-url`
## Related Guides
- [Authorization](https://docs.nextsign.dk/docs/authorization)
- [Rate Limits](https://docs.nextsign.dk/docs/rate-limits)
- [Test Environment](https://docs.nextsign.dk/docs/testenv)
### Rate Limits
Source: https://docs.nextsign.dk/docs/rate-limits
Summary: Understand the current NextSign API request limit and how to build retry-safe clients around it.
The current documented rate limit for the NextSign API is **100 requests per minute**.
Design your integration so it stays below the limit during normal traffic and during retries after transient failures.
## Practical Guidance
- Queue outbound requests from your backend instead of firing large bursts at once
- Limit concurrency for write operations such as case creation and form submission
- Cache repeated reads where it makes sense
- Avoid automatic retry storms after timeouts or network errors
## How to Handle Throttling
If your client is throttled or you receive temporary failures:
1. Stop sending immediate repeat requests
2. Retry with exponential backoff and jitter
3. Keep write operations idempotent on your side
4. Resume gradually instead of sending a full burst again
Suggested retry pattern for your own client:
- Retry 1: after 1 second
- Retry 2: after 2 seconds
- Retry 3: after 4 seconds
- Retry 4: after 8 seconds
- Retry 5: after 16 seconds
If your HTTP client receives a `Retry-After` header, follow that value before applying your own schedule.
## Planning for Production
Treat `100 requests per minute` as a hard planning ceiling for your integration. If your use case includes imports, bulk signing flows, or scheduled jobs, add buffering and queueing in your application layer.
## Test Environment
Validate your throughput strategy in the test environment before going live:
- confirm your retry behavior
- confirm your queue drains at an acceptable pace
- confirm your UI or backend degrades gracefully when requests are delayed
## Related Guides
- [Idempotency](https://docs.nextsign.dk/docs/idempotency)
- [Webhook Retry Logic](https://docs.nextsign.dk/docs/webhook-retry-logic)
- [Test Environment](https://docs.nextsign.dk/docs/testenv)
### Idempotency
Source: https://docs.nextsign.dk/docs/idempotency
Summary: Understand the difference between request idempotency, resource IDs, and webhook deduplication in NextSign.
Idempotency means a repeated operation should not create unintended duplicates or side effects. This matters when your client retries requests or when webhook deliveries are repeated.
## Request Idempotency
NextSign does **not** currently provide built-in request idempotency for create endpoints such as [Case Create](https://docs.nextsign.dk/docs/case-create) or [Forms Submit](https://docs.nextsign.dk/docs/forms-submit).
That means sending the same `POST` request twice can create two separate resources if both requests are accepted.
Do not treat `_id` or `referenceId` as a built-in idempotency key for requests. They do not prevent duplicate creates by themselves.
## What `_id` Means
`_id` is the unique identifier of a resource after it exists.
Use `_id` for:
- fetching an existing case, flow, or form
- storing relations between your records and NextSign resources
- correlating webhook payloads with resources already stored in your system
`_id` is resource identity, not request idempotency. You only know it after the resource has been created.
## What `referenceId` Means
Case creation and form submission also support `referenceId`. Use it as a business correlation field between your system and NextSign.
Typical uses:
- attach your internal order or contract number to a case
- trace retries and support cases more easily
- reconcile user actions and webhook events in your own system
`referenceId` is useful for your own matching and reconciliation, but it is not a uniqueness guarantee enforced by the API.
## Recommended Pattern for Create Requests
For create flows where the client might retry:
1. Generate your own operation ID before sending the request
2. Store the request intent in your own database
3. Send the request once
4. If the result is unknown, reconcile using your own stored context before retrying
5. Store the returned `_id` once the request succeeds
## Webhook Deduplication
Webhook deliveries can be repeated when delivery fails and is retried. Your webhook consumer should therefore be idempotent even though the create endpoints themselves are not request-idempotent.
Because the same case can emit multiple webhook events over time, a practical event-level deduplication key is:
```javascript
function getIdempotencyKey(payload) {
const entity = payload.case || payload.flow;
return [payload.trigger, entity?._id, entity?.updatedAt].filter(Boolean).join(':');
}
```
Store that key in your database or queue processor and ignore deliveries you have already handled successfully.
## Good Defaults
- make write operations originate from your backend, not directly from the browser
- log your own operation ID together with `referenceId` and the returned `_id`
- do not retry unknown `POST` outcomes blindly
- make webhook handlers safe to run more than once
## Related Guides
- [Webhook Retry Logic](https://docs.nextsign.dk/docs/webhook-retry-logic)
- [Webhooks](https://docs.nextsign.dk/docs/webhooks)
- [Rate Limits](https://docs.nextsign.dk/docs/rate-limits)
## Reference
Template syntax, shared payload objects, and lifecycle values reused across the API.
### Template Tags
Source: https://docs.nextsign.dk/docs/template-tags
Summary: The {tag} placeholder syntax used in .docx templates — values, conditions, lists, empty-state companions, dropdown options, recipient tags, global fields and the company logo tag.
NextSign fills `.docx` templates with data when a document is produced. Anywhere you write a **tag** in the Word file, the rendered document gets a value instead.
```text title="In Word"
This agreement is made between {company_name} and {kunde_navn}
on {today}.
```
Tags are what the `tags` field of [Create a Case from a Document](https://docs.nextsign.dk/docs/v3/documents-create-case) and [Forms Submit](https://docs.nextsign.dk/docs/forms-submit) fills. This page documents the syntax you write **in the Word file**; the endpoint pages document how to send the values.
Tag names are case-sensitive and must match exactly. A tag with no value renders as empty text — it is never left as `{tag}` in the output, and never fails the render.
---
## Value tags
A plain tag inserts a value:
```text
{kunde_navn}
```
Nested values use a dot:
```text
{kunde.adresse}, {kunde.by}
```
Values submitted through the API are inserted as you send them. Fields filled through a form or the dashboard are formatted according to the field type set in the tag editor — number fields get `.` as thousands separator, date fields render as `DD/MM/YYYY` unless the field defines its own format.
---
## Conditional sections
Wrap a block in `{#tag}` … `{/tag}` to include it only when the value is true:
```text
{#har_bilag}
Appendix A is attached to this agreement.
{/har_bilag}
```
`{^tag}` … `{/tag}` is the inverse — included only when the value is false or missing:
```text
{^har_bilag}
No appendices are attached.
{/har_bilag}
```
A `{#tag}` … `{/tag}` block **with no tags inside it** is read as a condition like this, and the tag becomes a yes/no field. The same block **with tags inside it** is read as a [list](#lists) instead.
If you number your sections **manually** ("3. Payment", "3.1 Prices"), NextSign renumbers the surviving sections after a conditional section is dropped, so the numbering stays consecutive. Word's own automatic numbering needs no help. Ambiguous documents are left untouched.
---
## Lists
A list value repeats the block between `{#tag}` and `{/tag}` once per item:
```text
{#ydelser}
{navn} — {pris} kr.
{/ydelser}
```
To repeat a **table row**, put `{#ydelser}` in the first cell of the row and `{/ydelser}` in the last cell. The row itself is repeated, not the text inside it.
A loop like this becomes a **list field**: rows the sender adds one by one in the dashboard, and an array of row objects through the API — see [Tags](https://docs.nextsign.dk/docs/v3/documents-create-case#tags) on the create-case endpoint. The tags inside the block (`{navn}`, `{pris}`) are the columns of each row.
---
## Empty-state companions: `_has_items` and `_filled`
Every tag automatically gets a boolean companion tag describing whether it has content:
| Companion | For | True when |
| --------- | --- | --------- |
| `{tag}_has_items` | lists | the list has at least one row |
| `{tag}_filled` | everything else | the value is non-empty |
Use them to gate **constant text** on whether content exists. A heading above a list should render once — not once per row, and not at all when the list is empty:
```text
{#ydelser_has_items}The following services are included:{/ydelser_has_items}
{#ydelser}{navn} — {pris} kr.{/ydelser}
{^ydelser_has_items}No additional services are included.{/ydelser_has_items}
```
The same works for optional single values — a table row wrapped in `{#depositum_filled}` … `{/depositum_filled}` disappears entirely when no deposit was agreed:
```text
{#depositum_filled}Deposit: {depositum} kr.{/depositum_filled}
```
Rules:
- Companions are computed at render time and are **never fields** — they don't appear in the tag editor, and the v3 endpoints reject them as unknown fields.
- A companion only exists when the document actually contains the base tag. A `{#foo_has_items}` section with no `{foo}` anywhere is treated as an ordinary [conditional section](#conditional-sections) with its own yes/no field.
- A real tag you define with that exact name always wins — the computed value never overwrites it.
- They work inside list rows too: within `{#ydelser}` … `{/ydelser}`, `{#kommentar_filled}` checks that row's own `kommentar`.
---
## Dropdown option tags
A dropdown field's answer also produces a boolean tag **per option**, so a document can check-mark the chosen option:
```text
Hovedkort {#karttype_hovedkort}✓{/karttype_hovedkort} Bikort {#karttype_bikort}✓{/karttype_bikort}
```
When the dropdown `karttype` is answered with "Hovedkort", `{karttype_hovedkort}` is true; every other option's tag stays false. The key is `{tag}_{option}` with the option value slugged: lowercase, spaces become `_`, and everything except `a–z`, `0–9`, `_` and `æøå` is dropped. Options containing æ/ø/å answer to an ASCII spelling as well — "Pædagog" on the dropdown `stilling` matches both `{stilling_pædagog}` and `{stilling_paedagog}`.
These are derived from the dropdown's answer at render time — the dropdown itself is the only field you fill or send, and a `{#karttype_hovedkort}` section in the document counts as covered by the `karttype` dropdown.
---
## Recipient tags
When a recipient carries a **naming slug** (the recipient-naming field in the form builder, e.g. `medarbejder`), that recipient's contact details are available as tags:
```text
{medarbejder_name} {medarbejder_email} {medarbejder_phone}
```
The sender never has to fill a separate field with data they already typed in the recipient step. The suffixes are always English (`_name`, `_email`, `_phone`), and a slug containing æ/ø/å also answers to its ASCII spelling (`{sælger_name}` and `{saelger_name}`).
A value supplied explicitly for such a tag always wins — the derived value only fills a tag that is missing or empty.
---
## Global fields
These are filled on every render — you never send values for them, they never appear as fields in the tag editor, and the v3 endpoints reject an attempt to set one (`"is a global field and is filled from the account"`). Times are in `Europe/Copenhagen`.
The registry is fixed and identical for every account — that is what makes a shared or partner template portable: `{company_name}` always means the sending company's name, on every account the template lands in.
### Date and time
| Tag | Value | Example |
| --- | ----- | ------- |
| `{today}` | Today's date | `31/07/2026` |
| `{date}` | Today's date, ISO | `2026-07-31` |
| `{datetime}` | Date and time | `31/07/2026 09:14:22` |
| `{time}` | Time | `09:14:22` |
| `{year}` | Year | `2026` |
| `{month}` | Month | `07` |
| `{day}` | Day of month | `31` |
| `{weekday}` | Weekday, in Danish | `fredag` |
| `{timestamp}` | Unix timestamp, seconds | `1785490126` |
### The sending company
Values come from the company profile. Each can be overridden under **Dashboard → Config** when the contract wording should differ from the profile (a formal legal name, say).
| Tag | Value |
| --- | ----- |
| `{company_name}` | Company name |
| `{company_cvr}` | CVR / registration number |
| `{company_address}` | Street address |
| `{company_zip}` | Postal code |
| `{company_city}` | City |
| `{company_country}` | Country name, e.g. `Danmark` |
| `{company_address_full}` | Full postal line: `Gade 1, 1234 By` |
| `{company_email}` | Company email |
| `{company_phone}` | Company phone |
### The sender
The person sending the case. Never overridable — the value follows whoever is acting.
| Tag | Value |
| --- | ----- |
| `{sender_name}` | Sender's name |
| `{sender_email}` | Sender's email |
| `{sender_phone}` | Sender's phone |
### Custom global fields
An account can define its own key/value pairs under **Dashboard → Config** — e.g. `{forhandler_navn}` — filled on every render just like the system fields. Keys are lowercase `snake_case`. This is what lets a partner ship one standard document that each customer fills with their own fixed details **once**, instead of on every case.
---
## Company logo
`{@company_logo}` inserts the logo of the company the document is produced for — the one uploaded under **Dashboard → Company → Details**. The same template therefore carries each company's own logo, which is what makes it usable for shared standard documents such as partner agreements.
```text title="In Word"
{@company_logo_w200_center}
PARTNER AGREEMENT
```
Note the `@`, which marks an image tag. Size and alignment are part of the tag name:
| Tag | Result |
| --- | ------ |
| `{@company_logo}` | 150 px wide, aspect ratio preserved |
| `{@company_logo_w200}` | 200 px wide |
| `{@company_logo_h60}` | 60 px high |
| `{@company_logo_200x60}` | fitted inside a 200 × 60 px box |
| `{@company_logo_w40mm}` | units: `px` (default), `mm`, `cm`, `pt` |
| `{@company_logo_w200_center}` | alignment: `left` (default), `center`, `right` |
Suffixes combine in any order: `{@company_logo_h60_right}`, `{@company_logo_40x20mm_center}`. The aspect ratio is always preserved — a width and a height together define a box the logo is fitted **inside**, never stretched to.
An image tag replaces the **entire paragraph** it sits in, so `{@company_logo}` must stand alone in its own paragraph (or its own table cell) — not inline in a sentence. For the same reason the paragraph's alignment in the template is not carried over: use the `_center` / `_right` suffix instead.
The tag works in the body, in table cells, and in headers and footers. When the company has no logo uploaded, the signature logo is used if one is configured; otherwise the paragraph is simply left empty. Logos uploaded in a format Word cannot embed (SVG, WebP) are converted automatically.
---
## Where to see a template's tags
- **Dashboard** — open the document and use its tag editor, or the API guide panel, which generates a pre-filled sample request.
- **API** — [Forms Tags](https://docs.nextsign.dk/docs/forms-tags) returns the tags a form expects.
Global fields, the `_has_items`/`_filled` companions, dropdown option tags, recipient tags and `{@company_logo}` are deliberately excluded from both — they are filled for you, so they are never fields you need to send.
### Objects
Source: https://docs.nextsign.dk/docs/objects
Summary: Shared request and response objects used across cases, forms, and document endpoints.
Use this page as the shared reference for the most important payload objects reused across the NextSign API.
This is the canonical place to look up:
- recipient fields
- document fields
- settings fields
- signing schema values
- case state values
## Recipient Object
Used in:
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Case Update](https://docs.nextsign.dk/docs/case-update)
- [Forms Submit](https://docs.nextsign.dk/docs/forms-submit)
- response payloads from [Case Get](https://docs.nextsign.dk/docs/case) and [Forms Get](https://docs.nextsign.dk/docs/forms-get)
### Common Request Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `name` | `string` | Recipient name |
| `email` | `string` | Recipient email address |
| `signing` | `boolean` | Whether the recipient must sign |
| `group` | `number` | Preferred signing order/group. Lower numbers are invited first |
| `order` | `number` | Legacy alias for `group` |
| `needsCpr` | `boolean` | Whether CPR validation is required |
| `cpr` | `string` | CPR number. Required when `needsCpr` is `true` |
| `signingSchema` | `string` | Optional recipient-specific signing method override |
| `redirectUrl` | `string` | Redirect URL after signing |
| `phone` | `string` | Phone number for SMS-style delivery flows |
| `position` | `string` | Signature title or position |
| `type` | `string` | Delivery type. Common values are `email`, `sms`/`phone`, and `eboks` |
| `template` | `string` | Optional recipient-specific template id |
| `message` | `string` | Custom recipient message |
| `eboks` | `object` | e-Boks delivery payload. See the [e-Boks Object](#e-boks-object) for `method`, `cvr`, `address`, and `name` |
### Update-Only Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `_id` | `string` | Existing recipient identifier when patching a recipient |
| `id` | `string` | Alias for `_id` |
| `clearCpr` | `boolean` | Set to `true` with an empty `cpr` value to clear stored CPR data |
### Common Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `_id` | `string` | Recipient identifier |
| `uid` | `string` | Internal recipient uid used in document signatory mapping |
| `signed` | `string` | Recipient signing status such as `pending`, `signed`, `denied`, or `not signing` |
| `signingToken` | `string` | Recipient signing token |
| `url` | `string` | Direct signing URL. Returned for open cases |
If `recipient.signingSchema` is omitted, the recipient falls back to the case-level `signingSchemas`.
If `recipient.signingSchema` is set, that recipient is locked to the selected method.
If `needsCpr` is `true`, you must also provide `cpr`.
If you need the plain CPR number later in your own workflow, store it on your side as well.
## e-Boks Object
The `eboks` object is nested inside a [recipient](#recipient-object) when `type` is set to `eboks`. It tells NextSign how to identify and reach the recipient inside the e-Boks system.
### Fields
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `method` | `string` | No | Identification method to use: `cpr`, `cvr`, or `address`. Defaults to `cpr` when omitted. |
| `cvr` | `string` | Conditional | CVR number for the recipient company. Required when `method` is `cvr`. |
| `address` | `string` | Conditional | Danish address string used to locate the recipient. Required when `method` is `address`. NextSign resolves the address into the underlying recipient details via the Danish address registry (dataforsyningen). |
| `name` | `string` | Conditional | Recipient name used alongside `address` for address-based delivery. Split into first name and last name by NextSign. |
| `kvhx` | `string` | No | Legacy KVHX identifier. Not required for current e-Boks delivery — the address is resolved server-side. See the note below. |
### Identification Methods
e-Boks delivery supports three ways to identify the recipient, selected with `eboks.method`. Set `type: "eboks"` on the recipient, then provide the field that matches the chosen method:
| `method` | Identifier field | Notes |
| -------- | ---------------- | ----- |
| `cpr` (default) | `recipient.cpr` | 10-digit CPR number for a private individual |
| `cvr` | `recipient.eboks.cvr` | 8-digit CVR number for a company |
| `address` | `recipient.eboks.address` | Resolved server-side through the Danish address registry |
An e-Boks recipient without any of these is rejected with a `400` before the case is created: `cpr` (or `eboks.cvr`) must be a 10-digit CPR or an 8-digit CVR (spaces and dashes are ignored), or `eboks.address` must be set. Only recipients whose type is `eboks` are checked, so an email or SMS recipient is never affected.
An e-Boks recipient is reached through e-Boks only. An `email` or `phone` on the same recipient is not used to send an invitation, so the letter does not also arrive as an email or SMS.
Whether e-Boks accepts the recipient (for example, whether the CPR is registered for Digital Post) is only known once we send. That result comes back in the create response (`delivery.eboks` in v3, a `notices` entry with code `eboks-delivery-failed` in v2) and as the [`case.delivery.failed` webhook](https://docs.nextsign.dk/docs/webhooks#delivery-failed-payload).
CPR/CVR identification is the most reliable method. Address-based delivery requires that the address can be resolved unambiguously by the Danish address registry. You do not need to supply a `kvhx` value — sending the `address` string is enough.
If the field required by the selected `method` is missing (a valid CPR/CVR number, or a resolvable `eboks.address`), the delivery will fail with an `invalid-recipient` error.
`kvhx` is a legacy field retained for backwards compatibility. It is only consumed by the older direct-dispatch path and is ignored by the current e-Boks delivery flow, which resolves the recipient from the `address` string itself. New integrations should not send `kvhx`.
### How the recipient name is resolved (address-based)
For address-based delivery, NextSign derives the recipient's first and last name from the recipient `name` field by splitting it on whitespace:
- **Two or more words** — everything except the final word becomes the first name, and the final word becomes the last name. The recipient is addressed with both first and last name. (e.g. `"Jane Marie Doe"` → first name `"Jane Marie"`, last name `"Doe"`)
- **A single word** — it is used as the first name only, with no last name. (e.g. `"Jane"` → first name `"Jane"`)
- **Empty** — the package is addressed to the resolved address without a personal name.
Because the last whitespace-separated token is treated as the last name, send the recipient `name` as `First [middle] Last`. A single-word `name` is delivered as a first name only.
### Example — CPR-based delivery
```json
{
"name": "Jane Doe",
"email": "jane@example.com",
"type": "eboks",
"signing": true,
"needsCpr": true,
"cpr": "1234567890",
"eboks": {
"method": "cpr"
}
}
```
### Example — CVR-based delivery
```json
{
"name": "Example Company ApS",
"email": "kontakt@example.com",
"type": "eboks",
"signing": true,
"eboks": {
"method": "cvr",
"cvr": "12345678"
}
}
```
### Example — Address-based delivery
```json
{
"name": "Jane Doe",
"email": "jane@example.com",
"type": "eboks",
"signing": true,
"eboks": {
"method": "address",
"address": "Østergade 12, 1100 København K",
"name": "Jane Doe"
}
}
```
### Fallback to physical letter
When a company has the letter-fallback option enabled and e-Boks delivery fails, NextSign automatically retries by sending a physical letter through the letter dispatch service. A warning notification is created on the case when fallback is triggered.
Whether letter fallback is attempted depends on your NextSign account configuration. Contact NextSign support to enable or disable this option.
## Document Object
Used in:
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Case Update](https://docs.nextsign.dk/docs/case-update)
- [Document Convert](https://docs.nextsign.dk/docs/documents-convert)
- response payloads from case endpoints
### Common Request Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `name` | `string` | File name including extension |
| `file` | `string` | Hosted file URL, uploaded file URL, or base64 file data depending on the endpoint |
| `fileIsBlob` | `boolean` | Set to `true` when `file` contains base64 file data |
| `signObligated` | `boolean` | Whether signing is required for the document |
| `documentMustBeRead` | `boolean` | Whether the document must be read before signing |
| `signatories` | `array` | Selected signatories for the document |
| `mergeTags` | `boolean` | [Case Create](https://docs.nextsign.dk/docs/case-create#template-tags) only. Set to `false` to leave a `.docx` out of the case's template-tag merge |
### Common Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `_id` | `string` | Stored document entry identifier |
| `document_id` | `string` | Stored document/file identifier |
| `file` | `string` | URL to the stored or converted file |
| `name` | `string` | Stored file name |
For [Document Convert](https://docs.nextsign.dk/docs/documents-convert), only `name` and `file` are required from this object.
If the combined case documents are more than **3.5 MB**, upload them first with [Document Upload](https://docs.nextsign.dk/docs/documents-upload) and use the returned URL in `file`.
After any recipient has signed, [Case Update](https://docs.nextsign.dk/docs/case-update) rejects document additions. Re-send a new contract/case instead.
## Settings Object
Used in:
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Case Update](https://docs.nextsign.dk/docs/case-update)
- [Forms Submit](https://docs.nextsign.dk/docs/forms-submit)
- response payloads from [Case Get](https://docs.nextsign.dk/docs/case) and [Forms Get](https://docs.nextsign.dk/docs/forms-get)
| Field | Type | Description |
| ----- | ---- | ----------- |
| `reminders.send` | `boolean` | Whether reminder messages should be sent |
| `reminders.amount` | `number` | Maximum number of reminders |
| `reminders.daysBetween` | `number` | Days between reminders |
| `deletion.autoDelete` | `boolean` | Whether the case should be automatically deleted later |
| `deletion.days` | `number` | Days before auto deletion |
| `availability.unlimited` | `boolean` | Whether the case should be available without an intended limit |
| `availability.days` | `number` | Days the case should stay available for signing |
| `availability.expiration` | `string` | ISO 8601 expiration timestamp managed by NextSign |
| `availability.isExpired` | `boolean` | Whether the case is currently expired |
| `template` | `number` | Signing page / document template selection |
| `lang` | `string` | Language code such as `da`, `en`, `de`, or `no` |
| `allowAPI` | `boolean` | Whether API usage is allowed in the related configuration context |
| `allowRecipientsViewOthers` | `boolean` | Whether recipients may view other participants |
| `allowedRoles` | `array` | Allowed roles list, primarily used with forms |
On case create and case update, NextSign recalculates `availability.expiration` from `availability.days`.
`availability.expiration` and `availability.isExpired` are managed by NextSign and should be treated as system values in responses.
## Signing Schemas
Used in:
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Case Update](https://docs.nextsign.dk/docs/case-update)
- [Forms Submit](https://docs.nextsign.dk/docs/forms-submit)
| Value | Name | CPR Required | Notes |
| ----- | ---- | ------------ | ----- |
| `urn:grn:authn:dk:mitid:low` | MitID Privat | No | MitID Privat for recipients with `needsCpr: false` |
| `urn:grn:authn:dk:mitid:substantial` | MitID Privat with CPR match | Yes | MitID Privat for recipients with `needsCpr: true` |
| `urn:grn:authn:dk:mitid:business` | MitID Erhverv | No | MitID business signing |
| `urn:grn:authn:se:bankid` | BankID SE | No | Swedish BankID signing |
| `urn:grn:authn:de:personalausweis` | Personalausweis | No | German national eID card (Personalausweis) signing |
| `draw` | Touch | No | Drawn signature without CPR verification |
| `draw-cpr` | Touch | Yes | Drawn signature with CPR verification |
| `sms-email` | SMS + Email verification | No | NextSign-hosted verification flow |
| `approve` | Godkendelse (click-approval) | No | Recipient-level only. The recipient approves the documents with a single click instead of signing — see the note below |
At the case level, `signingSchemas` defines the allowed signing methods for the case.
At the recipient level, `recipient.signingSchema` forces one specific method for that signer.
**A recipient is only offered the methods whose "CPR Required" value matches their own `needsCpr`.** A recipient with `needsCpr: false` never sees `urn:grn:authn:dk:mitid:substantial` or `draw-cpr`, and a recipient with `needsCpr: true` only sees those two.
`low` and `substantial` are not security levels: they are the same MitID Privat login without and with a CPR match. Sending `["urn:grn:authn:dk:mitid:substantial", "urn:grn:authn:dk:mitid:business"]` to recipients without `needsCpr` leaves them with MitID Erhverv only. If you want MitID Privat, send both `low` and `substantial` and NextSign picks the right one for each recipient.
**Approvers (`approve`)** — setting `recipient.signingSchema` to `approve` (with `signing: true`) makes the recipient an approver: they review the documents and approve with a single click (logged with timestamp, IP, and browser). An approval is not a signature — the approver never appears on the signed documents or the signature page. Approvers participate in the signing order like any signer: an approver in group 1 must approve before group 2 is contacted, and a rejection denies the whole case. An approval (or rejection) is charged 0.5 credits instead of 1. In responses, an approver who has approved has `status: "signed"`; distinguish approvers by their `signingSchema`.
`approve` is not accepted in the case-level `signingSchemas` list — it can only be forced per recipient.
After any recipient has signed, [Case Update](https://docs.nextsign.dk/docs/case-update) rejects `signingSchemas` changes. Re-send a new contract/case instead.
## Case States
Used in:
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Case Update](https://docs.nextsign.dk/docs/case-update)
- [Case Get](https://docs.nextsign.dk/docs/case)
### Canonical Lifecycle Values
| Value | Meaning |
| ----- | ------- |
| `draft` | The case exists but is not open for signing yet |
| `open` | The case is active and can be signed |
| `completed` | The case has been fully completed or manually marked as completed |
| `denied` | The case has been denied during the signing flow |
| `cancelled` | The case has been cancelled |
### Accepted Aliases On Case Update
| Value | Maps To |
| ----- | ------- |
| `pending` | `open` |
| `signed` | `completed` |
The `status` filter on [Case List](https://docs.nextsign.dk/docs/cases) is a different concept. It currently filters by derived signing progress (`pending`, `signed`, `denied`) rather than the stored lifecycle state.
## Related Endpoints
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Case Update](https://docs.nextsign.dk/docs/case-update)
- [Forms Submit](https://docs.nextsign.dk/docs/forms-submit)
- [Document Convert](https://docs.nextsign.dk/docs/documents-convert)
## Forms
Retrieve templates, inspect tags, and submit forms to create signing cases.
### Forms Get
Source: https://docs.nextsign.dk/docs/forms-get
Summary: Get a form by ID using GET /api/v2/{company}/forms/{form_id}/get.
Use this endpoint to retrieve a single form and inspect its settings, recipients, tags, and attached documents.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
GET https://www.nextsign.dk/api/v2/{company}/forms/{form_id}/get
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
| `form_id` | `string` | Unique identifier of the form |
## Example Request
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/forms/{form_id}/get' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
## Example Response
```json title="200 OK"
{
"_id": "65f493fb52bb6d8baa5d16af",
"companyId": "6576e19662ec13c4faa7de7b",
"folder": "Default",
"active": true,
"title": "Example Form",
"settings": {
"deletion": {
"autoDelete": true,
"days": 30
},
"availability": {
"unlimited": false,
"days": 10,
"isExpired": false
},
"reminders": {
"send": true,
"amount": 2,
"daysBetween": 3
},
"allowAPI": true
},
"tags": [],
"recipients": [],
"documents": []
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `_id` | `string` | Unique form identifier |
| `companyId` | `string` | Company identifier |
| `title` | `string` | Form title |
| `folder` | `string` | Folder name where the form is stored |
| `active` | `boolean` | Whether the form is active |
| `settings` | `object` | Form configuration settings |
| `tags` | `array` | Form tag definitions |
| `recipients` | `array` | Recipient configuration |
| `documents` | `array` | Documents attached to the form |
Shared object reference:
[Settings Object](https://docs.nextsign.dk/docs/objects#settings-object),
[Recipient Object](https://docs.nextsign.dk/docs/objects#recipient-object),
and [Document Object](https://docs.nextsign.dk/docs/objects#document-object).
## Common Error Responses
Form not found:
```json
{
"errors": [
{
"code": 400,
"message": "Bad Request",
"description": "Form not found"
}
]
}
```
Server error:
```json
{
"message": "Internal server error",
"errors": []
}
```
## Related Endpoints
- [Forms Tags](https://docs.nextsign.dk/docs/forms-tags)
- [Forms Submit](https://docs.nextsign.dk/docs/forms-submit)
- [Objects](https://docs.nextsign.dk/docs/objects)
### Forms Tags
Source: https://docs.nextsign.dk/docs/forms-tags
Summary: Get form tags using GET /api/v2/{company}/forms/{form_id}/tags/get.
Use this endpoint to retrieve the tags a form expects so you can construct a valid submit payload.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
GET https://www.nextsign.dk/api/v2/{company}/forms/{form_id}/tags/get
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
| `form_id` | `string` | Unique identifier of the form |
## Example Request
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/forms/{form_id}/tags/get' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
## Example Response
```json title="200 OK"
[
{
"name": "customer_name",
"tag": "customer_name",
"value": "",
"type": "text",
"_id": "65d34e9df381256d6be8cb78"
},
{
"name": "customer_zip",
"tag": "customer_zip",
"value": "",
"type": "text",
"_id": "65d34e9df381256d6be8cb76"
}
]
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `_id` | `string` | Unique tag identifier |
| `name` | `string` | Human-readable tag name |
| `tag` | `string` | Tag identifier used in the template |
| `value` | `string` | Default tag value |
| `type` | `string` | Tag type, for example `text`, `number`, or `date` |
## Common Error Responses
Company or form not found:
```json
{
"errors": [
{
"code": 400,
"message": "Bad Request",
"description": "Company not found"
}
]
}
```
Missing bearer token:
```json
{
"errors": [
{
"code": 401,
"message": "Unauthorized",
"description": "Bearer token not found"
}
]
}
```
## Related Endpoints
- [Forms Get](https://docs.nextsign.dk/docs/forms-get)
- [Forms Submit](https://docs.nextsign.dk/docs/forms-submit)
### Forms Submit
Source: https://docs.nextsign.dk/docs/forms-submit
Summary: Submit a form using POST /api/v2/{company}/forms/{form_id}/submit.
Use this endpoint to submit a form with tag values and recipients. A successful submission creates a case from the form configuration.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
POST https://www.nextsign.dk/api/v2/{company}/forms/{form_id}/submit
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
| `form_id` | `string` | Unique identifier of the form to submit |
## Request Body
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `title` | `string` | No | Override the default form title |
| `customMessage` | `boolean` | No | Whether to use a custom message |
| `message` | `string` | No | Custom message shown to recipients |
| `user_name` | `string` | No | Sender name |
| `user_email` | `string` | No | Sender email |
| `state` | `string` | No | Case state. Omit (or pass `open`) for a normal case that is created **and sent**. Pass `draft` to create the case as a draft that is **not** sent. See [Draft submissions](#draft-submissions). |
| `autoSend` | `boolean` | No | Whether to send the case to its recipients. Defaults to `true`, except when `state` is `draft` (then it defaults to `false`). Pass it explicitly to override. |
| `signingSchemas` | `array` | No | Allowed signature methods |
| `settings` | `object` | No | Case settings override |
| `folder` | `string` | No | Destination folder |
| `tags` | `array` | **Yes** | Tag values to inject into the form |
| `recipients` | `array` | **Yes** | Recipient list for the generated case |
Shared object reference:
[Settings Object](https://docs.nextsign.dk/docs/objects#settings-object),
[Signing Schemas](https://docs.nextsign.dk/docs/objects#signing-schemas),
[Recipient Object](https://docs.nextsign.dk/docs/objects#recipient-object),
and [Case States](https://docs.nextsign.dk/docs/objects#case-states).
The structure of `settings`, `signingSchemas`, and recipient objects follows the same pattern as [Case Create](https://docs.nextsign.dk/docs/case-create), including optional `recipient.signingSchema` overrides.
### Recipient Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `name` | `string` | Recipient name |
| `email` | `string` | Recipient email |
| `signing` | `boolean` | Whether the recipient must sign |
| `group` | `number` | Preferred signing order/group. Lower numbers are invited first |
| `order` | `number` | Legacy alias for `group` on forms submit |
| `needsCpr` | `boolean` | Whether CPR validation is required |
| `cpr` | `string` | Recipient CPR number. Required when `needsCpr` is `true` |
| `signingSchema` | `string` | Optional recipient-specific signing method override |
| `redirectUrl` | `string` | Redirect URL after signing. Must be sent inside the recipient object, and is returned back on `contract.recipients[].redirectUrl` in the response |
| `phone` | `string` | Phone number for SMS delivery |
| `position` | `string` | Signature title/position |
| `type` | `string` | Delivery type. Supported values are `email`, `phone`, and `eboks` |
| `template` | `string` | Optional recipient-specific template id |
| `message` | `string` | Custom recipient message |
| `eboks` | `object` | e-Boks delivery payload. See the [e-Boks Object](https://docs.nextsign.dk/docs/objects#e-boks-object) for `method`, `cvr`, `address`, and `name` |
If a submitted recipient uses `needsCpr: true`, you must also provide `recipient.cpr`. See [Case Create](https://docs.nextsign.dk/docs/case-create) for the full recipient schema and CPR handling notes.
## Draft submissions
By default a submission is created **and sent** to its recipients — the case is created with state `open` and signing invitations go out immediately.
To create the case as a **draft** instead — saved but not sent, so it can be reviewed or finished later — pass `state: "draft"`:
- The case is created with state `draft` and **no signing emails or SMS are sent**.
- The response `message` is `Contract created` (instead of `Contract created and sent`), and the `contract.recipients[].url` signing links are **not** included, since a draft is not open for signing yet.
- The draft appears alongside your other in-progress cases and can be opened, completed and sent later.
If you do **not** send `state` (or send `state: "open"`), the submission keeps the default behaviour and is sent off as normal.
`state: "draft"` implies `autoSend: false`. You can override this by sending `autoSend` explicitly — for example `state: "draft"` together with `autoSend: true` to create a draft-state case that is still sent.
## Example Request
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/forms/{form_id}/submit' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data-raw '{
"title": "Ny Kontrakt",
"state": "open",
"tags": [
{
"tag": "kunde_fornavn",
"value": "Andreas",
"type": "text"
},
{
"tag": "generel_price",
"value": 4500,
"type": "number"
}
],
"recipients": [
{
"name": "Andreas Lauridsen",
"email": "al@nextengine.dk",
"signing": true,
"group": 0,
"redirectUrl": "https://example.com/complete",
"signingSchema": "draw"
}
]
}'
```
### Example — draft submission
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/forms/{form_id}/submit' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data-raw '{
"title": "Kladde kontrakt",
"state": "draft",
"tags": [
{
"tag": "kunde_fornavn",
"value": "Andreas",
"type": "text"
}
],
"recipients": [
{
"name": "Andreas Lauridsen",
"email": "al@nextengine.dk",
"signing": true,
"group": 0
}
]
}'
```
```json title="200 OK — draft"
{
"status": "OK",
"message": "Contract created",
"contract": {
"_id": "66263484e601d81006d1a132",
"title": "Kladde kontrakt",
"state": "draft",
"folder": "Default",
"type": "Simple sign",
"recipients": [
{
"name": "Andreas Lauridsen",
"email": "al@nextengine.dk"
}
]
}
}
```
## Example Response
```json title="200 OK"
{
"status": "OK",
"message": "Contract created and sent",
"contract": {
"_id": "66263484e601d81006d1a132",
"title": "Ny Kontrakt",
"folder": "Default",
"type": "Simple sign",
"recipients": [
{
"name": "Andreas Lauridsen",
"email": "al@nextengine.dk",
"redirectUrl": "https://example.com/complete",
"url": "https://www.nextsign.dk/sign/66263484e601d81006d1a132/2/WGOfO-7xvoh-5LPvK-O9cVt-Q1GnY"
}
],
"createdAt": "2024-04-22T09:57:24.498Z",
"updatedAt": "2024-04-22T09:57:24.498Z"
}
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `status` | `string` | Request status, typically `OK` |
| `message` | `string` | Human-readable result message |
| `contract` | `object` | Created case object |
For open cases, the response includes `contract.recipients[].url`, which is the direct signing URL for each recipient.
If you submitted `recipient.redirectUrl`, the same value is returned on `contract.recipients[].redirectUrl` for that recipient.
## Common Error Responses
Validation error:
```json
{
"message": "Bad request",
"errors": [
"No ID provided"
]
}
```
Form not found:
```json
{
"errors": [
{
"code": 400,
"message": "Bad Request",
"description": "Form not found"
}
]
}
```
## Related Endpoints
- [Forms Get](https://docs.nextsign.dk/docs/forms-get)
- [Forms Tags](https://docs.nextsign.dk/docs/forms-tags)
- [Create a Case from a Document (API v3)](https://docs.nextsign.dk/docs/v3/documents-create-case)
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Objects](https://docs.nextsign.dk/docs/objects)
## Cases
Create, list, inspect, and manage signing cases across their lifecycle.
### Case List
Source: https://docs.nextsign.dk/docs/cases
Summary: List cases with filters using GET /api/v2/{company}/cases/get.
Use this endpoint to list cases for a company. You can filter by status, folder, and paginate the result set.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
GET https://www.nextsign.dk/api/v2/{company}/cases/get
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
## Query Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `status` | `string` | Filter by signing status: `signed`, `denied`, or `pending` |
| `limit` | `number` | Maximum number of cases to return. Default is `100` |
| `index` | `number` | Zero-based page index used together with `limit` |
| `folder` | `string` | Filter by folder name |
## Example Request
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/cases/get?status=signed&limit=10&index=0&folder=Default' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```javascript title="JavaScript"
const params = new URLSearchParams({
status: 'signed',
limit: '10',
index: '0',
folder: 'Default'
});
const response = await fetch(
`https://www.nextsign.dk/api/v2/{company}/cases/get?${params}`,
{
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
}
}
);
const data = await response.json();
```
```python title="Python"
import requests
url = "https://www.nextsign.dk/api/v2/{company}/cases/get"
headers = {
"Authorization": "Bearer YOUR_API_KEY"
}
params = {
"status": "signed",
"limit": 10,
"index": 0,
"folder": "Default"
}
response = requests.get(url, headers=headers, params=params)
data = response.json()
```
## Example Response
```json title="200 OK"
{
"status": "company_found",
"cases": [
{
"_id": "657b2ffb0965b111023fb0d1",
"title": "Contract Agreement",
"folder": "Default",
"createdAt": "2023-12-14T16:40:27.431Z",
"updatedAt": "2024-02-19T10:52:25.836Z"
}
]
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `status` | `string` | Status of the lookup, typically `company_found` |
| `cases` | `array` | Array of matching case objects |
The API returns `404` when no cases match the filter.
## Common Error Responses
Invalid company id:
```json
{
"code": 400,
"message": "Bad Request",
"description": "Invalid company id"
}
```
No matching cases:
```json
{
"message": "Case not found"
}
```
## Related Endpoints
- [Case Get Multiple](https://docs.nextsign.dk/docs/cases-get-multiple)
- [Case Get](https://docs.nextsign.dk/docs/case)
- [Case Create](https://docs.nextsign.dk/docs/case-create)
### Case Get Multiple
Source: https://docs.nextsign.dk/docs/cases-get-multiple
Summary: Get multiple cases by ID using GET /api/v2/{company}/cases/{case_ids}/get.
Use this endpoint to fetch multiple specific cases in a single request by passing a comma-separated list of case IDs.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
GET https://www.nextsign.dk/api/v2/{company}/cases/{case_ids}/get
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
| `case_ids` | `string` | Comma-separated list of case IDs |
## Example Request
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/cases/6618ddb22f3dfc40f20af288,661cf690d0a8e1ab7c2c951c/get' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
## Example Response
```json title="200 OK"
{
"status": "cases_found",
"cases": [
{
"_id": "6618ddb22f3dfc40f20af288",
"title": "Contract 1",
"createdAt": "2023-12-14T16:40:27.431Z",
"updatedAt": "2024-02-19T10:52:25.836Z"
},
{
"_id": "661cf690d0a8e1ab7c2c951c",
"title": "Contract 2",
"createdAt": "2024-01-10T08:20:15.123Z",
"updatedAt": "2024-03-05T14:30:45.678Z"
}
]
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `status` | `string` | Status of the lookup, typically `cases_found` |
| `cases` | `array` | Array of matching case objects |
The API returns `404` when none of the requested IDs are found for the company.
## Common Error Responses
Missing bearer token:
```json
{
"errors": [
{
"code": 401,
"message": "Unauthorized",
"description": "Bearer token not found"
}
]
}
```
No matching cases:
```json
{
"message": "Cases not found"
}
```
## Related Endpoints
- [Case List](https://docs.nextsign.dk/docs/cases)
- [Case Get](https://docs.nextsign.dk/docs/case)
### Case Get
Source: https://docs.nextsign.dk/docs/case
Summary: Get a single case by ID using GET /api/v2/{company}/case/{case_id}/get.
Use this endpoint to fetch a single case by its unique identifier.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
GET https://www.nextsign.dk/api/v2/{company}/case/{case_id}/get
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
| `case_id` | `string` | Unique identifier of the case |
## Example Request
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/case/657b2ffb0965b111023fb0d1/get' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```javascript title="JavaScript"
const response = await fetch(
'https://www.nextsign.dk/api/v2/{company}/case/657b2ffb0965b111023fb0d1/get',
{
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
}
}
);
const data = await response.json();
```
```python title="Python"
import requests
url = "https://www.nextsign.dk/api/v2/{company}/case/657b2ffb0965b111023fb0d1/get"
headers = {
"Authorization": "Bearer YOUR_API_KEY"
}
response = requests.get(url, headers=headers)
data = response.json()
```
## Example Response
```json title="200 OK"
{
"status": "case_found",
"case": {
"settings": {
"deletion": {
"autoDelete": true,
"days": 30
},
"availability": {
"days": 10,
"isExpired": false,
"unlimited": false
},
"reminders": {
"amount": 2,
"daysBetween": 3,
"send": true
},
"allowAPI": false,
"allowedRoles": []
},
"_id": "657b2ffb0965b111023fb0d1",
"user_name": "Daniel Prior",
"user_email": "123@live.dk",
"title": "Example Agreement",
"referenceId": "REF-48392",
"state": "open",
"type": "Simple sign",
"createdAt": "2023-12-14T16:40:27.431Z",
"updatedAt": "2024-02-19T10:52:25.836Z"
}
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `status` | `string` | Status of the request, typically `case_found` |
| `case` | `object` | Case object containing the current case data |
| `case._id` | `string` | Unique case identifier |
| `case.title` | `string` | Case title |
| `case.referenceId` | `string` | Internal reference identifier |
| `case.state` | `string` | Current lifecycle state such as `open`, `completed`, or `denied` |
| `case.type` | `string` | Case type |
| `case.createdAt` | `string` | ISO 8601 creation timestamp |
| `case.updatedAt` | `string` | ISO 8601 update timestamp |
| `case.settings` | `object` | Case settings, including reminders and availability |
Shared object reference:
[Settings Object](https://docs.nextsign.dk/docs/objects#settings-object)
and [Case States](https://docs.nextsign.dk/docs/objects#case-states).
## Common Error Responses
Invalid company id:
```json
{
"errors": [
{
"code": 400,
"message": "Bad Request",
"description": "Invalid company id"
}
]
}
```
Case not found (`404`):
```json
{
"message": "Case not found"
}
```
## Related Endpoints
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Case Update](https://docs.nextsign.dk/docs/case-update)
- [Case Delete](https://docs.nextsign.dk/docs/case-delete)
- [Case List](https://docs.nextsign.dk/docs/cases)
- [Objects](https://docs.nextsign.dk/docs/objects)
### Case Create
Source: https://docs.nextsign.dk/docs/case-create
Summary: Create a case using POST /api/v2/{company}/case/create.
Use this endpoint to create a case with recipients, settings, and documents for signing.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
POST https://www.nextsign.dk/api/v2/{company}/case/create
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
## Request Body
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `title` | `string` | **Yes** | Case title |
| `referenceId` | `string` | No | Internal reference ID |
| `folder` | `string` | No | Folder name |
| `autoSend` | `boolean` | No | Send immediately after creation |
| `customMessage` | `boolean` | No | Enable a custom recipient message |
| `message` | `string` | No | Custom recipient message |
| `user_email` | `string` | No | Sender email address |
| `emailtemplate` | `string` | No | Email template ID. An unknown id is ignored (see [Template Not Found](#template-not-found)) |
| `settings` | `object` | **Yes** | Case settings |
| `signingSchemas` | `array` | No | Allowed signature methods |
| `recipients` | `array` | **Yes** | Recipient list |
| `documents` | `array` | **Yes** | Documents to sign |
| `tags` | `array` | No | Values filled into `{tag}` placeholders in `.docx` documents |
Shared object reference:
[Settings Object](https://docs.nextsign.dk/docs/objects#settings-object),
[Signing Schemas](https://docs.nextsign.dk/docs/objects#signing-schemas),
[Recipient Object](https://docs.nextsign.dk/docs/objects#recipient-object),
[Document Object](https://docs.nextsign.dk/docs/objects#document-object),
and [Case States](https://docs.nextsign.dk/docs/objects#case-states).
### Signing Schemas
- `urn:grn:authn:dk:mitid:substantial` (MitID Privat with CPR match, only offered to recipients with `needsCpr: true`)
- `urn:grn:authn:dk:mitid:low` (MitID Privat, only offered to recipients with `needsCpr: false`)
- `urn:grn:authn:dk:mitid:business`
- `urn:grn:authn:se:bankid`
- `urn:grn:authn:de:personalausweis`
- `draw-cpr`
- `draw`
- `sms-email`
- `approve` (recipient-level only — see [Approvers](#approvers))
Each recipient is only offered the methods that fit their `needsCpr`. `low` and `substantial` are the same MitID Privat login without and with a CPR match, not two security levels, so a list with `substantial` but not `low` gives recipients without `needsCpr` no MitID Privat option at all. Send both and NextSign picks the right one per recipient. See [Signing Schemas](https://docs.nextsign.dk/docs/objects#signing-schemas).
### Recipient Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `name` | `string` | Recipient name |
| `email` | `string` | Recipient email |
| `signing` | `boolean` | Whether the recipient must sign |
| `group` | `number` | Preferred signing order/group. Lower numbers are invited first |
| `order` | `number` | Legacy alias for `group` on create endpoints |
| `needsCpr` | `boolean` | Whether CPR validation is required |
| `cpr` | `string` | Recipient CPR number. Required when `needsCpr` is `true` |
| `signingSchema` | `string` | Optional recipient-specific signing method override |
| `redirectUrl` | `string` | Redirect URL after signing. Must be sent inside the recipient object |
| `phone` | `string` | Phone number for SMS delivery |
| `position` | `string` | Signature title/position |
| `type` | `string` | Delivery type. Supported values are `email`, `phone`, and `eboks` |
| `template` | `string` | Optional recipient-specific template id. An unknown id falls back to the case template (see [Template Not Found](#template-not-found)) |
| `message` | `string` | Custom recipient message |
| `eboks` | `object` | e-Boks delivery payload. See the [e-Boks Object](https://docs.nextsign.dk/docs/objects#e-boks-object) for `method`, `cvr`, `address`, and `name` |
`recipient.signingSchema` accepts the same values listed under `Signing Schemas`.
If `recipient.signingSchema` is omitted, the recipient falls back to the case-level `signingSchemas`. If it is set, that recipient is locked to the selected method and cannot choose other signing options during signing.
If `needsCpr` is `true` and `recipient.signingSchema` is set, CPR verification still uses `urn:grn:authn:dk:mitid:substantial` before the signer continues with the forced signing method.
If `needsCpr` is `true`, you must also send `recipient.cpr` in the payload. If `recipient.cpr` is missing, the signer cannot complete CPR verification.
If you need the CPR number later in your own workflow, store it on your side as well. Do not rely on NextSign as a source for recovering the original plain-text CPR value after processing.
Example recipient with CPR validation:
```json
{
"name": "Andreas",
"email": "al@nextsign.dk",
"cpr": "190497xxxx",
"needsCpr": true,
"signing": true
}
```
### Approvers
An approver reviews the documents and approves them with a single click — no eID, no signature. Approvals are logged (timestamp, IP, browser) but the approver never appears on the signed documents or the signature page. An approval or rejection is charged **0.5 credits** instead of 1.
Create an approver by forcing the `approve` method on the recipient:
```json
{
"name": "Andreas",
"email": "al@nextsign.dk",
"signing": true,
"signingSchema": "approve",
"group": 1
}
```
Approvers participate in the signing order like signers: an approver in group `1` must approve before group `2` is contacted. If the approver rejects, the whole case is denied and the case owner is notified with the rejection reason.
`approve` cannot be used in the case-level `signingSchemas` list — it is a per-recipient method only. `needsCpr` is not supported for approvers.
### Document Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `name` | `string` | File name including extension |
| `file` | `string` | Hosted file URL, uploaded file URL, or base64 data |
| `fileIsBlob` | `boolean` | Set to `true` for base64 payloads. Omit it when using an uploaded file URL |
| `signObligated` | `boolean` | Whether signing is required |
| `documentMustBeRead` | `boolean` | Whether the document must be read before signing |
| `signatories` | `array` | Selected signatories |
| `mergeTags` | `boolean` | Set to `false` to leave this `.docx` out of the template-tag merge. See [Template Tags](#template-tags) |
Documents can be **PDF**, **Word** (`.docx`), or **iXBRL / XML** (`.xhtml`, `.html`, `.xml`). iXBRL annual reports for the Danish Business Authority (Erhvervsstyrelsen) are supported and signed with an enveloped XAdES seal. See [Accepted File Types](https://docs.nextsign.dk/docs/documents-upload#accepted-file-types).
If the combined size of the documents in a case is more than **3.5 MB**, upload the files first with [Document Upload](https://docs.nextsign.dk/docs/documents-upload) and use the returned URL in `documents[].file`.
### Template Tags
When the request body contains a non-empty `tags` array, **every** `.docx` in `documents` is rendered with those values before it is converted to PDF — `{customer_name}` in the Word file becomes the value you sent. PDF documents are never affected, and a request without `tags` converts each `.docx` as-is.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `tag` | `string` | Tag name as written in the Word file, without braces |
| `value` | `string \| number \| array` | Value to insert |
| `type` | `string` | Tag type such as `text`, `number`, `date`, `condition`, or `array` |
| `name` | `string` | Optional human-readable label stored on the case |
| `sensitive` | `boolean` | Encrypt the value at rest on the case. The plain value is still used for the merge |
See [Template Tags](https://docs.nextsign.dk/docs/template-tags) for the `{tag}` placeholder syntax to write in the Word file, including conditional sections and lists.
Set `mergeTags: false` on an individual document to leave that file out of the merge. Use it for appendices that travel with the main contract but are not built from the same template — the file is converted to PDF unchanged, so unrelated `{` characters in it cannot break the render.
```json
{
"tags": [
{ "tag": "customer_name", "value": "Andreas", "type": "text" }
],
"documents": [
{
"name": "Lejekontrakt.docx",
"file": "BASE64_DATA",
"fileIsBlob": true
},
{
"name": "Bilag 1.docx",
"file": "BASE64_DATA",
"fileIsBlob": true,
"mergeTags": false
}
]
}
```
`mergeTags` only has an effect on this endpoint, and only on `.docx` documents in a request that also sends `tags`. Omitting it means the document takes part in the merge, so existing payloads keep their current behaviour.
### Large Document Upload Flow
1. Upload the file with [Document Upload](https://docs.nextsign.dk/docs/documents-upload)
2. Take the `url` from the upload response
3. Pass that URL into `documents[].file` in the case creation payload
Example:
```json
{
"documents": [
{
"name": "Lejekontrakt.pdf",
"file": "https://nextsign-dev.hel1.your-objectstorage.com/.../Lejekontrakt.pdf"
}
]
}
```
## Example Request
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/case/create' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data-raw '{
"title": "Lejeaftale",
"referenceId": "Jrn. 2342-23",
"folder": "Default",
"autoSend": true,
"customMessage": true,
"message": "Kære {recipient_name}, som aftalt er hermed dokumenter til underskrift",
"user_email": "info@nextengine.dk",
"settings": {
"reminders": {
"send": true,
"amount": 2,
"daysBetween": 3
},
"lang": "da",
"deletion": {
"autoDelete": false,
"days": 30
},
"availability": {
"unlimited": true,
"days": 10
},
"template": 2
},
"signingSchemas": [
"urn:grn:authn:dk:mitid:low",
"urn:grn:authn:dk:mitid:substantial",
"urn:grn:authn:dk:mitid:business",
"draw"
],
"recipients": [
{
"name": "Andreas Lauridsen",
"email": "al@nextengine.dk",
"signing": true,
"group": 0,
"redirectUrl": "https://example.com/complete",
"signingSchema": "urn:grn:authn:dk:mitid:business"
}
],
"documents": [
{
"name": "Lejekontrakt.pdf",
"file": "YOUR_BASE64_STRING_OR_URL",
"fileIsBlob": true
}
]
}'
```
## Example Response
```json title="200 OK"
{
"data": {
"user": "65ab12cd34ef56ab78cd90ef",
"nextSignKey": "7QmLp2Xs8HdNa4VrTc9YwKbJf",
"realtime": false,
"user_email": "api@example.com",
"user_company": "65ab12cd34ef56ab78cd90a1",
"title": "Lejeaftale",
"referenceId": "REF-48392",
"bin": false,
"type": "Simple sign",
"folder": "Default",
"folderId": "65ab12cd34ef56ab78cd90a2",
"signingSchemas": [
"urn:grn:authn:dk:mitid:substantial",
"urn:grn:authn:dk:mitid:low",
"urn:grn:authn:dk:mitid:business",
"urn:grn:authn:se:bankid",
"draw-cpr",
"draw"
],
"settings": {
"deletion": {
"autoDelete": false,
"days": 30
},
"availability": {
"unlimited": false,
"days": 10,
"expiration": "2026-05-11T08:54:36.371Z",
"isExpired": false
},
"reminders": {
"send": true,
"amount": 2,
"daysBetween": 3
},
"template": 2,
"lang": "da",
"allowedRoles": [],
"allowAPI": false,
"allowRecipientsViewOthers": true
},
"recipients": [
{
"group": 0,
"name": "Mia Sørensen",
"needsCpr": false,
"redirectUrl": "https://example.com/complete",
"position": "Director",
"cpr": "U2FsdGVkX18nR4mQ2Lp6YvZ8nT1aKs5JwP3eHx9LmNo=",
"email": "mia.soerensen@example.com",
"phone": "28123456",
"sort": 0,
"template": "",
"type": "email",
"signing": true,
"signed": "pending",
"signingToken": "Qp7Lm2Va9XrTc4Hs8KdNy5WbE",
"signingSchema": "",
"smsEmailVerification": {
"attempts": 0
},
"signer": {
"signer_type": "recipient",
"identity": {
"confirmed": false,
"cprIsMatch": false
}
},
"message": {
"enable": true,
"content": "Insert Custom message per Recipient here."
},
"uid": "pL8xRt2Q",
"_id": "65ab12cd34ef56ab78cd90b1",
"emailSent": [],
"log": [],
"emailEvents": [],
"events": [],
"url": "https://www.nextsign.dk/sign/65ab12cd34ef56ab78cd90b0/2/Qp7Lm2Va9XrTc4Hs8KdNy5WbE"
}
],
"integrations": {
"microsoft": {
"status": false
},
"uniconta": {
"status": false
}
},
"errorEmailSent": false,
"reminders": {
"allSigned": false,
"createdAt": "2026-05-01T08:54:36.578Z"
},
"documents": [
{
"name": "Lejekontrakt.pdf",
"file": "https://nextsign.hel1.your-objectstorage.com/65ab12cd34ef56ab78cd90a1/case/1777625676406-Lejekontrakt.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=R4ND0MCR3D3NT14LKEY1%2F20260501%2Fhel1%2Fs3%2Faws4_request&X-Amz-Date=20260501T085437Z&X-Amz-Expires=3600&X-Amz-Signature=9f1c2d3a4b5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
"document_id": "https://nextsign.hel1.your-objectstorage.com/65ab12cd34ef56ab78cd90a1/case/1777625676406-Lejekontrakt.pdf",
"signObligated": true,
"documentMustBeRead": true,
"signatories": [],
"_id": "65ab12cd34ef56ab78cd90b2"
}
],
"roles": [
"admin",
"økonomi",
"salg",
"marketing",
"standard",
"it",
"support",
"hr"
],
"message": "Kære {recipient_name}, som aftalt er hermed dokumenter til underskrift",
"customMessage": true,
"autoSend": true,
"reminderAutoSend": true,
"attachFiles": true,
"source": {
"type": "api"
},
"state": "open",
"_id": "65ab12cd34ef56ab78cd90b0",
"tags": [],
"errors": [],
"otherDocuments": [],
"signedDocuments": [],
"logs": [],
"createdAt": "2026-05-01T08:54:36.581Z",
"updatedAt": "2026-05-01T08:54:36.581Z",
"__v": 0,
"id": "65ab12cd34ef56ab78cd90b0"
},
"notices": [
{
"message": "No user id provided, adding the email.",
"using": "65ab12cd34ef56ab78cd90ef"
},
{
"message": "No user name provided"
},
{
"message": "No roles provided - default roles used",
"using": [
"admin",
"økonomi",
"salg",
"marketing",
"standard",
"it",
"support",
"hr"
]
},
{
"message": "No email template provided - using default"
}
]
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `data` | `object` | Created case object |
| `notices` | `array` | Non-fatal notices returned during processing |
### e-Boks Delivery Failures
An `open` case with `autoSend` on is sent before the response is returned, so an e-Boks recipient that e-Boks rejects (a CPR not registered for Digital Post, an address that cannot be resolved) is reported in `notices`. The status is still `200`: the case exists, so do not retry the request.
```json
{
"code": "eboks-delivery-failed",
"message": "e-Boks delivery to Andreas Lauridsen failed: invalid-address",
"recipientId": "65ab12cd34ef56ab78cd90b1",
"error": "invalid-address"
}
```
There is one notice per failed recipient. The same failure also fires the [`case.delivery.failed` webhook](https://docs.nextsign.dk/docs/webhooks#delivery-failed-payload).
If the invitation email could not be sent, the case is still created and the response is still `200`, with this notice:
```json
{ "code": "email-delivery-failed", "message": "The invitation email could not be sent" }
```
An e-Boks recipient is reached through e-Boks only: an `email` or `phone` on it is not used for an invitation.
### Template Not Found
An unknown template id, for example a deleted template or a mistyped id, is ignored, and the case is still created with status `200`. The case falls back to the default copy, and a recipient's `template` falls back to the case template. Each ignored id gets a notice that names the field:
```json
{
"code": "message-template-not-found",
"field": "recipients[1].template",
"message": "recipients[1].template was not found for this company and was ignored"
}
```
The [v3 API](https://docs.nextsign.dk/docs/v3/case-create) rejects the request instead, with the `message-template-not-found` error.
## Common Error Responses
Validation error:
```json
{
"errors": [
{
"message": "Missing recipients"
}
],
"notices": []
}
```
An e-Boks recipient without a CPR, CVR or address (see the [e-Boks Object](https://docs.nextsign.dk/docs/objects#e-boks-object)) is rejected before the case is created. This covers recipients that get the `eboks` type from the `emailtemplate`, and only recipients of type `eboks`:
```json
{
"errors": [
{
"message": "recipients[0].cpr is required for an e-Boks recipient, unless eboks.cvr or eboks.address is given",
"recipient": 0,
"field": "cpr"
}
],
"notices": []
}
```
Server error:
```json
{
"errors": [
{
"message": "Server error",
"error": "Internal error details"
}
]
}
```
## Related Endpoints
- [Case Get](https://docs.nextsign.dk/docs/case)
- [Case Update](https://docs.nextsign.dk/docs/case-update)
- [Case Delete](https://docs.nextsign.dk/docs/case-delete)
- [Document Upload](https://docs.nextsign.dk/docs/documents-upload)
- [Objects](https://docs.nextsign.dk/docs/objects)
- [Forms Submit](https://docs.nextsign.dk/docs/forms-submit)
### Case Update
Source: https://docs.nextsign.dk/docs/case-update
Summary: Update an existing case using PATCH /api/v2/{company}/case/{case_id}/update.
Use this endpoint to update a case after it has been created. You can change the case lifecycle status, update general case settings, patch an existing recipient, append new recipients, and append documents.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
`PATCH` is the documented method for this endpoint. `POST` is also accepted for backwards compatibility.
---
## Endpoint
```text
PATCH https://www.nextsign.dk/api/v2/{company}/case/{case_id}/update
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
| `case_id` | `string` | Unique identifier of the case to update |
## Request Body
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `title` | `string` | No | Update the case title |
| `referenceId` | `string` | No | Update or clear the internal reference ID |
| `status` | `string` | No | Public lifecycle status. Supported values: `draft`, `open`, `completed`, `denied`, `cancelled`. Aliases `pending` -> `open` and `signed` -> `completed` are also accepted |
| `state` | `string` | No | Alias for `status` using canonical lifecycle values |
| `completedReason` | `string` | No | Optional reason stored when the case is marked as `completed` |
| `settings` | `object` | No | Merge into the existing case settings. Availability expiration is recalculated from `settings.availability.days` |
| `signingSchemas` | `array` | No | Replace the case-level signing methods. Each recipient only sees the ones that fit their `needsCpr`, see [Signing Schemas](https://docs.nextsign.dk/docs/objects#signing-schemas). Blocked after any recipient has signed |
| `recipient` | `object` | No | Patch one existing recipient. `recipient._id` or `recipient.id` is required. Blocked after any recipient has signed |
| `recipients` | `array` | No | Append new recipients to the case. Blocked after any recipient has signed |
| `addDocuments` | `array` | No | Append new documents to the case. Blocked after any recipient has signed |
Shared object reference:
[Settings Object](https://docs.nextsign.dk/docs/objects#settings-object),
[Signing Schemas](https://docs.nextsign.dk/docs/objects#signing-schemas),
[Recipient Object](https://docs.nextsign.dk/docs/objects#recipient-object),
[Document Object](https://docs.nextsign.dk/docs/objects#document-object),
and [Case States](https://docs.nextsign.dk/docs/objects#case-states).
`status` on this endpoint changes the case lifecycle itself. This is different from the `status` filter on [Case List](https://docs.nextsign.dk/docs/cases), which describes signing progress derived from the recipients on the case.
Changing `status` or `state` here updates the stored case lifecycle directly. It does not send cancellation emails or run other side effects from dedicated dashboard actions such as the separate cancel flow.
### Important After First Signature
Once any recipient has `signed`, the signing package should be treated as locked.
The update endpoint rejects these contract-shape changes after the first signature:
- Changing `signingSchemas`
- Updating `recipient`
- Adding `recipients`
- Adding `addDocuments`
If you need to make one of those changes after someone has signed, re-send a new contract/case instead of editing the existing one.
### Possible States
| Value | Meaning |
| ----- | ------- |
| `draft` | The case exists but is not open for signing yet |
| `open` | The case is active and can be signed |
| `completed` | The case has been marked as completed or fully finished |
| `denied` | The case has been denied during the signing flow |
| `cancelled` | The case has been cancelled |
Alias values accepted on `status`:
- `pending` maps to `open`
- `signed` maps to `completed`
### Recipient Patch Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `_id` | `string` | Existing recipient identifier |
| `id` | `string` | Alias for `_id` |
| `name` | `string` | Updated recipient name |
| `email` | `string` | Updated recipient email |
| `phone` | `string` | Updated recipient phone number |
| `needsCpr` | `boolean` | Enable or disable CPR validation |
| `cpr` | `string` | New CPR number. It is encrypted before storage |
| `clearCpr` | `boolean` | Set to `true` together with an empty `cpr` value to clear stored CPR data |
| `signing` | `boolean` | Whether the recipient must sign |
| `signingSchema` | `string` | Force a recipient-specific signing method |
| `group` | `number` | Preferred signing order/group |
| `order` | `number` | Legacy alias for `group` |
| `template` | `string` | Recipient-specific template id. An unknown id is ignored and the recipient keeps its current template (see [Template Not Found](#template-not-found)) |
| `type` | `string` | Delivery type such as `email`, `sms`, or `eboks` |
| `eboks` | `object` | e-Boks delivery payload. See the [e-Boks Object](https://docs.nextsign.dk/docs/objects#e-boks-object) for `method`, `cvr`, `address`, and `name` |
| `redirectUrl` | `string` | Redirect URL after signing |
### Add Recipient Fields
New recipients appended with `recipients` use the same structure as [Case Create](https://docs.nextsign.dk/docs/case-create#recipient-fields). An unknown `template` falls back to the case template (see [Template Not Found](#template-not-found)).
### Add Document Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `name` | `string` | File name including extension |
| `file` | `string` | Hosted file URL or base64 file data |
| `fileIsBlob` | `boolean` | Set to `true` when `file` contains base64 data |
| `signObligated` | `boolean` | Whether signing is required for the document |
## Example Request
```bash title="cURL"
curl --location --request PATCH 'https://www.nextsign.dk/api/v2/{company}/case/{case_id}/update' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data-raw '{
"status": "completed",
"completedReason": "Handled manually outside NextSign",
"title": "Lejeaftale - afsluttet",
"referenceId": "Jrn. 2342-23B",
"settings": {
"reminders": {
"send": false
},
"availability": {
"days": 14
}
},
"signingSchemas": [
"urn:grn:authn:dk:mitid:low",
"urn:grn:authn:dk:mitid:substantial",
"draw"
],
"recipient": {
"_id": "65ab12cd34ef56ab78cd90af",
"email": "kunde@example.com",
"signingSchema": "urn:grn:authn:dk:mitid:low"
}
}'
```
## Example Response
```json title="200 OK"
{
"success": true,
"status": "case_updated",
"errors": [],
"notices": [],
"newDocuments": [],
"case": {
"_id": "657b2ffb0965b111023fb0d1",
"title": "Lejeaftale - afsluttet",
"referenceId": "Jrn. 2342-23B",
"state": "completed",
"signingSchemas": [
"urn:grn:authn:dk:mitid:low",
"urn:grn:authn:dk:mitid:substantial",
"draw"
],
"settings": {
"deletion": {
"autoDelete": true,
"days": 30
},
"availability": {
"days": 14,
"isExpired": false,
"unlimited": false,
"expiration": "2026-06-24T09:10:14.271Z"
},
"reminders": {
"send": false,
"amount": 2,
"daysBetween": 3
}
},
"updatedAt": "2026-06-10T09:10:14.294Z"
}
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `success` | `boolean` | Indicates whether the update was applied |
| `status` | `string` | Operation status, typically `case_updated` |
| `errors` | `array` | Validation or processing errors |
| `notices` | `array` | Informational messages |
| `newDocuments` | `array` | Documents appended during this update request |
| `case` | `object` | Updated case payload |
| `case.state` | `string` | Stored lifecycle status of the case |
### Template Not Found
An unknown template id, for example a deleted template or a mistyped id, is ignored, and the update still returns `200`. A patched recipient keeps its current template, and an appended recipient gets the case template. Each ignored id gets a notice that names the field:
```json
{
"code": "message-template-not-found",
"field": "recipient.template",
"message": "recipient.template was not found for this company and was ignored"
}
```
## Common Error Responses
Invalid lifecycle status:
```json
{
"success": false,
"errors": [
{
"message": "Invalid status. Use draft, open, completed, denied, cancelled, pending, or signed."
}
],
"notices": [],
"newDocuments": []
}
```
Blocked after first signature:
```json
{
"success": false,
"errors": [
{
"message": "Cannot add documents after a recipient has signed. Re-send a new contract instead."
}
],
"notices": [],
"newDocuments": []
}
```
Case not found (`404`):
```json
{
"message": "Case not found"
}
```
Recipient not found:
```json
{
"message": "Recipient not found"
}
```
## Related Endpoints
- [Case Get](https://docs.nextsign.dk/docs/case)
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Case Delete](https://docs.nextsign.dk/docs/case-delete)
- [Objects](https://docs.nextsign.dk/docs/objects)
### Case Delete
Source: https://docs.nextsign.dk/docs/case-delete
Summary: Delete a case using GET /api/v2/{company}/case/{case_id}/delete.
Use this endpoint to delete a case by its unique identifier.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
GET https://www.nextsign.dk/api/v2/{company}/case/{case_id}/delete
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
| `case_id` | `string` | Unique identifier of the case to delete |
## Example Request
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/{company}/case/{case_id}/delete' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
## Example Response
```json title="200 OK"
{
"status": "removed",
"return": {
"acknowledged": true,
"deletedCount": 1
}
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `status` | `string` | Status of the deletion operation |
| `return` | `object` | Deletion result from the database |
| `return.acknowledged` | `boolean` | Whether the delete operation was acknowledged |
| `return.deletedCount` | `number` | Number of deleted cases |
## Common Error Responses
Missing case id:
```json
{
"errors": [
{
"code": 400,
"message": "Bad Request",
"description": "Missing case id"
}
]
}
```
Case not found (`404`):
```json
{
"message": "Case not found"
}
```
## Related Endpoints
- [Case Get](https://docs.nextsign.dk/docs/case)
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Case Update](https://docs.nextsign.dk/docs/case-update)
## Documents
Upload larger files, retrieve signed files, and convert source documents before they are sent.
### Document Upload
Source: https://docs.nextsign.dk/docs/documents-upload
Summary: Upload a file using POST /v3/company/{company}/file/upload.
Use this endpoint to upload a file before case creation.
If the combined size of the documents you want to send in a case is more than **3.5 MB**, upload the files first and pass the returned URL into `documents[].file` in [Case Create](https://docs.nextsign.dk/docs/case-create).
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
POST https://api.nextsign.dk/v3/company/{company}/file/upload
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
## Request Formats
This endpoint supports both:
- `multipart/form-data`
- Base64 payloads in the request body
## Accepted File Types
- `application/pdf` — PDF
- `application/vnd.openxmlformats-officedocument.wordprocessingml.document` — Word (`.docx`)
- `application/xhtml+xml` — iXBRL / Inline XBRL annual reports (`.xhtml`)
- `text/html` — iXBRL (`.html`)
- `application/xml`, `text/xml` — XML (`.xml`)
**iXBRL (Inline XBRL)** is the mandatory format for annual reports filed with the Danish Business Authority (Erhvervsstyrelsen) for balance dates on or after 1 January 2025. Upload the `.xhtml` file like any other document and add it to a case — it is signed with an enveloped XAdES seal (PDFs use a PAdES seal) and stays both human-readable and machine-readable.
## Multipart Form Data Fields
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `file` | `file` | **Yes** | File to upload |
| `name` | `string` | No | File name to store with the upload |
| `type` | `string` | No | Valid MIME type such as `application/pdf` |
## Base64 Body Fields
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `file` | `string` | **Yes** | Base64 string or full data URL |
| `name` | `string` | **Yes** | File name to store with the upload |
| `type` | `string` | Conditionally | MIME type. Optional when `file` is a full data URL like `data:application/pdf;base64,...` |
For base64 uploads, send either a full data URL such as `data:application/pdf;base64,...` or a raw base64 string plus an explicit `type`.
## Example Request
```bash title="cURL"
curl --location 'https://api.nextsign.dk/v3/company/{company}/file/upload' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-F 'file=@"/path/to/Hi.pdf"' \
-F 'name=Hi.pdf' \
-F 'type=application/pdf'
```
## Example Request With Base64
```json title="JSON Body"
{
"file": "data:application/pdf;base64,JVBERi0xLjQKJcfs...",
"name": "Hi.pdf"
}
```
```bash title="cURL - Base64"
curl --location 'https://api.nextsign.dk/v3/company/{company}/file/upload' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{
"file": "data:application/pdf;base64,JVBERi0xLjQKJcfs...",
"name": "Hi.pdf"
}'
```
## Example Response
```json title="200 OK"
{
"url": "https://nextsign-dev.hel1.your-objectstorage.com/686557955db9ec08e6a94c39/1774622990607-Hi.pdf",
"type": "application/pdf"
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `url` | `string` | Uploaded file URL to use in later API requests |
| `type` | `string` | MIME type stored for the uploaded file |
## Use In Case Create
Pass the returned `url` into the `documents[].file` field:
```json
{
"documents": [
{
"name": "Hi.pdf",
"file": "https://nextsign-dev.hel1.your-objectstorage.com/686557955db9ec08e6a94c39/1774622990607-Hi.pdf"
}
]
}
```
## Related Endpoints
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Document Retrieve](https://docs.nextsign.dk/docs/documents)
## Common Error Responses
Missing file:
```json
{
"error": "missing-file"
}
```
Missing file name:
```json
{
"error": "missing-file-name"
}
```
Missing file type:
```json
{
"error": "missing-file-type"
}
```
Invalid file type:
```json
{
"error": "invalid-file-type"
}
```
### Document Retrieve
Source: https://docs.nextsign.dk/docs/documents
Summary: Retrieve a pre-signed document URL using POST /v3/company/{company}/file/view-presigned-url.
Use this endpoint to generate a pre-signed URL for a case document stored in NextSign object storage.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
POST https://api.nextsign.dk/v3/company/{company}/file/view-presigned-url
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
## Request Body
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `url` | `string` | **Yes** | Stored document URL that should be converted into a pre-signed URL |
## Example Request
```bash title="cURL"
curl --location 'https://api.nextsign.dk/v3/company/{company}/file/view-presigned-url' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{
"url": "https://nextsign.hel1.your-objectstorage.com/655e252f708ff057141e577e/case/example.pdf"
}'
```
## Example Response
```json title="200 OK"
{
"key": "655e252f708ff057141e577e/case/example.pdf",
"signedUrl": "https://nextsign.hel1.your-objectstorage.com/655e252f708ff057141e577e/case/example.pdf?...",
"type": "application/pdf",
"cached": true
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `key` | `string` | Object storage key for the file |
| `signedUrl` | `string` | Time-limited pre-signed URL |
| `type` | `string` | MIME type of the file, when available |
| `cached` | `boolean` | Whether the response came from the URL cache |
The generated URL is cached for up to 1 hour.
## Common Error Responses
Missing file URL:
```json
{
"error": "missing-url"
}
```
## Related Endpoints
- [Create a Case from a Document (API v3)](https://docs.nextsign.dk/docs/v3/documents-create-case)
- [Document Upload](https://docs.nextsign.dk/docs/documents-upload)
- [Document Convert](https://docs.nextsign.dk/docs/documents-convert)
### Document Convert
Source: https://docs.nextsign.dk/docs/documents-convert
Summary: Convert documents using POST /api/v2/document/convert.
Use this endpoint to convert DOCX documents to PDF and replace tags before sending or previewing a document.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint.
---
## Endpoint
```text
POST https://www.nextsign.dk/api/v2/document/convert
```
## Request Body
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `tags` | `array` | No | Tag replacements used when converting DOCX files |
| `documents` | `array` | **Yes** | Array of documents to convert |
### Tag Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `tag` | `string` | Template tag name |
| `value` | `string \| number` | Replacement value |
| `type` | `string` | Tag type such as `text`, `number`, or `date` |
| `format` | `string` | Optional date format |
### Document Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `file` | `string` | URL to the source document |
| `name` | `string` | Document name |
See [Document Object](https://docs.nextsign.dk/docs/objects#document-object) for the shared document reference used across case and document endpoints.
## Example Request
```bash title="cURL"
curl --location 'https://www.nextsign.dk/api/v2/document/convert' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{
"tags": [
{
"tag": "customer_name",
"value": "Andreas",
"type": "text"
},
{
"tag": "contract_number",
"value": 200,
"type": "number"
}
],
"documents": [
{
"file": "https://example.com/path/to/contract.docx",
"name": "Contract Agreement.docx"
}
]
}'
```
## Example Response
```json title="200 OK"
{
"status": {
"code": 200,
"message": "OK"
},
"documents": [
{
"file": "https://example.com/path/to/converted-contract.pdf",
"name": "Contract Agreement.pdf"
}
]
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `status.code` | `number` | HTTP-style status code in the payload |
| `status.message` | `string` | Status message |
| `documents` | `array` | Converted documents |
| `documents[].file` | `string` | URL to the converted file |
| `documents[].name` | `string` | Converted file name |
PDF files are passed through, while DOCX files are converted to PDF.
## Common Error Responses
Missing documents array:
```json
{
"message": "Bad request",
"errors": [
{
"status": "error",
"message": "minimum of 1 documents is required"
}
]
}
```
## Related Endpoints
- [Case Create](https://docs.nextsign.dk/docs/case-create)
- [Case Update](https://docs.nextsign.dk/docs/case-update)
- [Objects](https://docs.nextsign.dk/docs/objects)
## Events
Subscribe to lifecycle notifications and understand webhook delivery retries.
### Webhooks
Source: https://docs.nextsign.dk/docs/webhooks
Summary: Receive real-time notifications when case and flow events occur in NextSign.
NextSign webhooks let NextSign push case and flow events to your system in near real time. Use them when you want your backend to react to signing activity without polling the API.
## Common Use Cases
- Update internal systems when a case is created, signed, or denied
- Start document archival or downstream processing after signing
- Keep CRM or ERP records in sync with NextSign activity
- React to reminder and expiry events in your own workflow engine
- Handle completed webflows without polling for status changes
---
## Where to Configure Webhooks
Manage webhooks from the **Webhooks** page in the NextSign dashboard.
From there you can create, edit, enable or disable webhooks, configure custom headers, and inspect recent delivery runs.
---
## Creating a Webhook
### Name
Use a descriptive internal name so the webhook is easy to identify in the dashboard.
**Example:** `Signed case webhook`
---
### Triggers (Required)
Select one or more trigger values. Each selected trigger can independently deliver a webhook request to the same URL.
| Label | Trigger Value | Description |
| ----- | ------------- | ----------- |
| **Case created** | `case_created` | Sent when a case is created |
| **Recipient signed** | `recipient_signed` | Sent when a recipient signs |
| **Recipient approved** | `recipient_approved` | Sent when an approver (recipient with `signingSchema: "approve"`) approves the documents |
| **Case signed** | `case_signed` | Sent when the case is fully signed |
| **Case denied** | `case_denied` | Sent when a recipient denies the case |
| **Workflow completed** | `webflow.completed` | Sent when a webflow completes |
| **Case reminder sent** | `case.reminder.sent` | Sent when a reminder email is sent |
| **Case expired** | `case.expiry.expired` | Sent when a case expires |
| **Case expiry 7 days** | `case.expiry.7days` | Sent when a case is 7 days from expiry |
| **Delivery failed** | `case.delivery.failed` | Sent when a delivery to a recipient fails: an e-Boks delivery that is rejected, for example because the CPR is not registered for Digital Post, or a case email (invitation or reminder) that bounces. Carries an `event` object, see [Delivery Failed Payload](#delivery-failed-payload) |
You can attach multiple trigger values to a single webhook configuration.
---
### URL (Required)
This is the public endpoint on your server that will receive webhook deliveries.
```text
https://example.com/webhooks/nextsign
```
Localhost URLs will not receive webhook traffic unless you expose them with a tunneling service such as ngrok.
---
### Method
NextSign supports `POST`, `GET`, `PUT`, and `DELETE` delivery methods.
- Default: `POST`
- Recommended: `POST`
If you do not configure a `Content-Type` header yourself, NextSign sends `application/json`.
---
### Active
Use the active toggle to enable or pause delivery without deleting the webhook configuration.
- **On**: events are delivered
- **Off**: events are ignored for this webhook
---
### Headers (Optional)
Custom headers are sent exactly as configured in the dashboard.
**Example:**
| Key | Value |
| --- | ----- |
| `X-Webhook-Secret` | `your-shared-secret` |
| `X-API-Version` | `2026-03` |
Common uses:
- Pass a shared secret your server can validate
- Add routing metadata for your integration
- Version your internal webhook consumer
NextSign does not generate an HMAC signature automatically for webhook requests. If you want to verify origin, configure a shared secret header such as `X-Webhook-Secret` and compare the exact header value in your handler.
---
## Delivery Behavior
When a selected trigger fires, NextSign sends an HTTP request to the configured URL. The request body contains the machine-readable trigger name plus either a `case` object or a `flow` object, depending on what caused the webhook.
### Expected Response
Your endpoint should:
- Return a `2xx` HTTP status code to acknowledge successful delivery
- Respond quickly and offload slow work to background jobs or queues
- Treat retries as possible duplicate deliveries
### Retry Logic
If delivery fails or your endpoint returns a non-`2xx` response, NextSign retries using exponential backoff.
- First retry: about 2 minutes after the failed attempt
- Then approximately: 4, 8, 16, 32 minutes, and so on
- Retries continue until delivery succeeds or the retry limit is reached
- The current retry limit is up to 14 total delivery attempts
Webhook deliveries can be repeated when retries happen. Make your webhook handler idempotent.
For a focused guide, see [Webhook Retry Logic](https://docs.nextsign.dk/docs/webhook-retry-logic).
---
## Payload Structure
### Case Event Payload
Case-related triggers send a `case` object:
```json title="Example Case Payload"
{
"trigger": "case_signed",
"case": {
"_id": "66263484e601d81006d1a132",
"title": "Contract Agreement",
"updatedAt": "2024-01-15T14:30:00.000Z",
"recipients": [
{
"name": "Andreas Lauridsen",
"email": "al@nextengine.dk"
}
],
"signedDocuments": [
{
"name": "Contract.pdf",
"file": "https://nextsign.hel1.your-objectstorage.com/..."
}
]
}
}
```
### Flow Event Payload
Flow-related triggers send a `flow` object:
```json title="Example Flow Payload"
{
"trigger": "webflow.completed",
"flow": {
"_id": "66a3c3bdb42398dff0d10123",
"title": "Lead Intake",
"updatedAt": "2024-01-15T14:30:00.000Z"
}
}
```
### Delivery Failed Payload
`case.delivery.failed` fires once per failed delivery to a recipient, next to the usual `case` (or `flow`) object. The `event` object says who it was, through which channel, and why. `channel` is `eboks` or `email`.
#### e-Boks
```json title="Example Delivery Failed Payload (e-Boks)"
{
"trigger": "case.delivery.failed",
"case": { "_id": "66263484e601d81006d1a132", "title": "Contract Agreement" },
"event": {
"channel": "eboks",
"recipientId": "66263484e601d81006d1a133",
"recipientName": "Andreas Lauridsen",
"errorMessage": "invalid-address"
}
}
```
`errorMessage` is either e-Boks' own rejection text (for example when the recipient is not registered for Digital Post) or one of these codes: `invalid-cpr`, `invalid-recipient`, `invalid-address`, `invalid-integration` (e-Boks is not set up for the company) and `invalid-template`.
A delivery that fell back to a physical letter was delivered, so it does not fire this trigger.
#### Email
Fires when a case email to a recipient bounces. That is the invitation or a reminder.
```json title="Example Delivery Failed Payload (email)"
{
"trigger": "case.delivery.failed",
"case": { "_id": "66263484e601d81006d1a132", "title": "Contract Agreement" },
"event": {
"channel": "email",
"recipientId": "66263484e601d81006d1a133",
"recipientName": "Andreas Lauridsen",
"email": "andreas@exampel.dk",
"bounceType": "hard",
"errorMessage": "Host or domain name not found"
}
}
```
| Field | Description |
| ----- | ----------- |
| `email` | The address that bounced. Compare it with the recipient's current address: if the address was corrected in the meantime, this bounce is about the old one |
| `bounceType` | `hard`: the address does not exist, or the receiving server refused it permanently. `soft`: a temporary problem, such as a full mailbox, that did not clear while our email provider kept retrying. Either way, that email will not arrive |
| `errorMessage` | The receiving server's reason when it gave one, otherwise `email-bounced` |
| `fallbackChannel` | Only present when the recipient was part of a mass sending with a fallback order, and the case was resent to them through this channel (`sms` or `eboks`). A failure on that channel is reported on its own |
The same bounce also shows on the case itself, as `recipients[].emailStatus` in [Retrieve a Case](https://docs.nextsign.dk/docs/v3/case-get#email-status).
There is no webhook for an email being opened. Opens are recorded in `emailStatus`, but they are too unreliable to act on, see the note there.
### Payload Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `trigger` | `string` | Machine-readable trigger value such as `case_signed` |
| `case` | `object` | Present for case-related triggers |
| `flow` | `object` | Present for flow-related triggers |
| `case._id` | `string` | Case identifier, when `case` is present |
| `flow._id` | `string` | Flow identifier, when `flow` is present |
The `case` or `flow` object contains the current resource data at delivery time. Exact fields can vary depending on the trigger and the resource state.
---
## Implementing a Webhook Endpoint
### Node.js / Express
```javascript title="Node.js / Express"
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/nextsign', (req, res) => {
if (req.headers['x-webhook-secret'] !== process.env.NEXTSIGN_WEBHOOK_SECRET) {
return res.status(401).send('Unauthorized');
}
const { trigger, case: webhookCase, flow } = req.body;
const entity = webhookCase || flow;
switch (trigger) {
case 'case_signed':
console.log(`Case signed: ${entity?._id}`);
break;
case 'case_denied':
console.log(`Case denied: ${entity?._id}`);
break;
case 'webflow.completed':
console.log(`Flow completed: ${entity?._id}`);
break;
default:
console.log(`Unhandled trigger: ${trigger}`);
}
res.status(200).send('OK');
});
app.listen(3000);
```
### Python / Flask
```python title="Python / Flask"
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
@app.route('/webhooks/nextsign', methods=['POST'])
def webhook():
if request.headers.get('X-Webhook-Secret') != os.getenv('NEXTSIGN_WEBHOOK_SECRET'):
return jsonify({'error': 'Unauthorized'}), 401
payload = request.json or {}
trigger = payload.get('trigger')
entity = payload.get('case') or payload.get('flow')
if trigger == 'case_signed':
print(f"Case signed: {entity.get('_id')}")
elif trigger == 'case_denied':
print(f"Case denied: {entity.get('_id')}")
elif trigger == 'webflow.completed':
print(f"Flow completed: {entity.get('_id')}")
else:
print(f"Unhandled trigger: {trigger}")
return jsonify({'status': 'ok'}), 200
if __name__ == '__main__':
app.run(port=3000)
```
---
## Best Practices
### Use HTTPS
Serve your webhook endpoint over HTTPS so request data stays encrypted in transit.
### Validate a Shared Secret Header
If you configure a header such as `X-Webhook-Secret`, compare it directly against a secret stored in your environment.
### Process Asynchronously
Return a success response quickly, then hand off heavier work such as document downloads, CRM sync, or notifications to background processing.
### Implement Idempotency
A practical idempotency key can be built from the trigger plus the resource ID and update timestamp.
```javascript
function getIdempotencyKey(payload) {
const entity = payload.case || payload.flow;
return [payload.trigger, entity?._id, entity?.updatedAt].filter(Boolean).join(':');
}
```
See [Idempotency](https://docs.nextsign.dk/docs/idempotency) for request-side guidance as well.
---
## Testing Webhooks
### Local Development with ngrok
Expose your local server:
```bash
ngrok http 3000
```
Then configure the generated HTTPS URL in the NextSign dashboard.
### Manual Test Request
```bash
curl -X POST https://your-domain.com/webhooks/nextsign \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: your-shared-secret" \
-d '{
"trigger": "case_signed",
"case": {
"_id": "test-case-123",
"title": "Test Agreement",
"updatedAt": "2024-01-15T14:30:00.000Z"
}
}'
```
---
## Troubleshooting
### No Webhooks Arrive
- Verify the webhook is active in the dashboard
- Confirm the URL is public and reachable
- Confirm the correct trigger values are selected
- Review webhook runs in the dashboard to see failed attempts
### 401 or 403 Responses
- Verify the shared secret header name matches exactly
- Verify the expected header value matches the configured value
- Check for proxy or load balancer rules stripping custom headers
### Duplicate Deliveries
- Expect retries after failures or timeouts
- Use idempotency keys when storing or processing events
### Timeouts
- Return `200 OK` quickly
- Move slow processing to workers or queues
- Avoid downloading large files inline before acknowledging the webhook
Use the webhook runs view in the dashboard to inspect recent deliveries, statuses, and retry behavior.
### Webhook Retry Logic
Source: https://docs.nextsign.dk/docs/webhook-retry-logic
Summary: Understand how NextSign retries failed webhook deliveries and how to build duplicate-safe handlers.
If a webhook delivery fails, NextSign retries automatically using exponential backoff.
## What Counts as a Failed Delivery
A delivery is treated as failed when:
- the destination cannot be reached
- the request times out
- your endpoint returns a non-`2xx` HTTP response
## Delivery Schedule
NextSign attempts delivery immediately, then retries with increasing delay when the delivery fails.
Typical schedule:
- Attempt 1: immediate delivery
- Attempt 2: about 2 minutes later
- Attempt 3: about 4 minutes later
- Attempt 4: about 8 minutes later
- Attempt 5: about 16 minutes later
- Further attempts continue with exponential backoff
The current limit is **up to 14 total delivery attempts** for a webhook event.
Webhook delivery is at-least-once, not exactly-once. Your handler must tolerate duplicate deliveries.
## Recommended Handler Design
- return `2xx` as soon as the payload is accepted
- move slow work into a queue or background worker
- deduplicate events before performing side effects
- log failures with enough detail to replay safely
## Example Deduplication Key
```javascript
function getIdempotencyKey(payload) {
const entity = payload.case || payload.flow;
return [payload.trigger, entity?._id, entity?.updatedAt].filter(Boolean).join(':');
}
```
## Operational Advice
- do not download large files before acknowledging the webhook
- do not send emails or sync external systems inline if the work can be queued
- monitor repeated failures so you can fix broken endpoints before the retry window is exhausted
## Related Guides
- [Webhooks](https://docs.nextsign.dk/docs/webhooks)
- [Idempotency](https://docs.nextsign.dk/docs/idempotency)
## Getting Started
What the v3 beta covers, and how to authenticate against it.
### Overview
Source: https://docs.nextsign.dk/docs/v3
Summary: Create and send documents for signing from your own system, with the NextSign API.
The NextSign API lets your system create a signing case — a title, the people who must sign, and the documents they sign — and hands you back a signing link for each recipient. Everything you create appears in the NextSign dashboard, and fires the same [webhooks](https://docs.nextsign.dk/docs/webhooks) as work started there.
**Beta.** The behaviour described here is stable, but the surface is still small and details may change before general availability. The gaps are listed at the bottom of this page.
## Base URL
```text
https://api.nextsign.dk/v3/api
```
## What you can do today
| Endpoint | Status |
| -------- | ------ |
| [`POST /v3/api/cases`](https://docs.nextsign.dk/docs/v3/case-create) — create and send a case | Available |
| [`GET /v3/api/cases`](https://docs.nextsign.dk/docs/v3/case-list) — list your cases | Available |
| [`GET /v3/api/cases/{id}`](https://docs.nextsign.dk/docs/v3/case-get) — retrieve one case | Available |
| [`GET /v3/api/documents`](https://docs.nextsign.dk/docs/v3/documents-list) — list your document library | Available |
| [`GET /v3/api/documents/{id}`](https://docs.nextsign.dk/docs/v3/documents-get) — a document and the fields it needs filled | Available |
| [`POST /v3/api/documents/{id}/cases`](https://docs.nextsign.dk/docs/v3/documents-create-case) — fill a library document and send it | Available |
| [`POST /v3/api/webflows`](https://docs.nextsign.dk/docs/v3/webflows-create) — request information by email | Available |
| [`GET /v3/api/webflows/{id}`](https://docs.nextsign.dk/docs/v3/webflows-get) — retrieve progress and answers | Available |
| Update and delete cases | Not yet |
| Uploading to and managing the document library; creating and editing forms | Not yet |
## What to expect
**Requests fail loudly.** Unknown fields are rejected rather than ignored, and every problem in the body comes back in one response, each with a `field` path pointing at the value that caused it.
**A `2xx` means the case exists.** You never get an error for a case that was already created. If sending fails you still get `201`, plus a per-channel `delivery` report — so a retry is always safe, and a briefly unavailable mail server never leaves you with two cases.
**Responses are explicit.** Every response is a defined object with a documented set of fields, so you can rely on what is there and on what is not.
**Keys are real credentials.** They are generated from a CSPRNG, stored hashed, and support revocation and expiry. See [Authorization](https://docs.nextsign.dk/docs/v3/authorization).
## Where to start
## Not available yet
Being explicit about the gaps, so you can plan around them:
- **No rate limiting is applied yet.** Do not treat that as a licence to hammer the API; a limit is coming.
- **Case creation has no idempotency key.** Retrying a `201` creates a second case. Retry only on network failures and `5xx`, never on a `4xx`. Webflow creation requires an idempotency key; see [webflow retries](https://docs.nextsign.dk/docs/v3/webflows-create#retries-and-invitation-status).
- **Scopes are not enforced.** Every key currently has full access to its company. Per-key scopes are planned; keys issued today will keep working when they arrive.
- **No search when listing cases.** You can filter by state, folder, reference and creation date, but not by title or recipient. Filter on your own `referenceId` instead.
- **The document library is read-only.** You can list documents, read their fields and create cases from them, but uploading a new template or version still happens in the dashboard.
### Authorization
Source: https://docs.nextsign.dk/docs/v3/authorization
Summary: Create an API key, send it with every request, and understand why some keys are rejected.
Every request is authenticated with an API key, and that is the only credential you need to send.
## Create an API Key
### Open the dashboard
Go to **Dashboard → Config → API Keys**.
### Create the key
Click **Create API Key** and give it a name that identifies the system using it, such as `Billing Service`.
### Copy it now
Keys look like this, and this is the only time you will see the whole thing:
```text
key_Wb22OsfMPpEkAHeS6LHqBWoaFkpEuCdyZx3pt5RvICo
```
The full key is shown **once**, at creation. After that only a masked form (`key_A1b2C3d4…xY9z`) is visible. If you lose it, revoke the key and create a new one.
## Send the key
Three header forms are accepted, so you can reuse whichever convention your HTTP client already has:
```http
Authorization: Bearer key_xxx
Authorization: key key_xxx
Authorization: key_xxx
```
```bash title="cURL"
curl --location 'https://api.nextsign.dk/v3/api/cases' \
-H "Authorization: Bearer ${NEXTSIGN_API_KEY}" \
-H 'Content-Type: application/json' \
--data-raw '{ "title": "Test", "state": "draft" }'
```
A key belongs to exactly one company, and everything you create with it belongs to that company. Use a separate key per system, so you can revoke one without disturbing the others.
## Who your cases belong to
Every case is attributed to a person, and that decides two real things: which colleagues can see it — through the folder's role list — and who the signing email says it came from.
By default that person is whoever created the key, and the response says so with a `sender-defaulted-to-key-creator` notice. To attribute a case to a specific colleague instead, send `senderId` or `senderEmail` — see [Sender](https://docs.nextsign.dk/docs/v3/case-create#sender). That user must be a member of the same company.
If one key serves a whole team, name the sender on every request. Otherwise every case is owned by — and appears to come from — the colleague who happened to create the key.
## Authentication errors
All of these are returned before your request body is looked at.
| Status | `error` | Meaning |
| ------ | ------- | ------- |
| `401` | `missing-token` | No `Authorization` header |
| `401` | `invalid-token` | The key does not exist |
| `401` | `key-revoked` | The key was revoked in the dashboard |
| `401` | `key-expired` | The key passed its expiry date |
| `403` | `company-disabled` | The company account is disabled |
```json title="401 Unauthorized"
{
"error": "invalid-token"
}
```
`key-revoked` and `key-expired` are deliberately distinguished from `invalid-token`. You already hold the key, so this reveals nothing — and it saves you debugging an integration that was simply turned off or aged out.
## Storing keys
```bash title=".env"
NEXTSIGN_API_KEY=key_your_key_here
NEXTSIGN_V3_URL=https://api.nextsign.dk/v3/api
```
Keys are stored hashed on our side and cannot be recovered — not by you, and not by support. Treat one as you would a database password: keep it out of source control, out of client-side code, and out of logs.
Revoking a key in the dashboard takes effect immediately on the next request.
## Related
## Cases
Create and read signing cases with strict validation and explicit responses.
### Create a Case
Source: https://docs.nextsign.dk/docs/v3/case-create
Summary: Create and send a signing case with POST /v3/api/cases — full request and response reference.
Creates a case with its recipients and documents, and — unless you ask for a draft — sends it.
```text
POST https://api.nextsign.dk/v3/api/cases
```
Authenticated with an API key — see [Authorization](https://docs.nextsign.dk/docs/v3/authorization).
## Quickstart
The smallest request that creates and sends a case: a title, one recipient, one document.
```bash
curl https://api.nextsign.dk/v3/api/cases \
-H "Authorization: Bearer $NEXTSIGN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Rental agreement",
"recipients": [{ "name": "Andreas Lauridsen", "email": "al@example.com" }],
"documents": [{ "name": "contract.pdf", "content": "JVBERi0xLjQK..." }]
}'
```
```js
const response = await fetch('https://api.nextsign.dk/v3/api/cases', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NEXTSIGN_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Rental agreement',
recipients: [{ name: 'Andreas Lauridsen', email: 'al@example.com' }],
documents: [{ name: 'contract.pdf', content: pdfBuffer.toString('base64') }],
}),
});
const { case: created } = await response.json();
console.log(created.recipients[0].signingUrl);
```
```python
import base64, os, requests
pdf = base64.b64encode(open("contract.pdf", "rb").read()).decode()
response = requests.post(
"https://api.nextsign.dk/v3/api/cases",
headers={"Authorization": f"Bearer {os.environ['NEXTSIGN_API_KEY']}"},
json={
"title": "Rental agreement",
"recipients": [{"name": "Andreas Lauridsen", "email": "al@example.com"}],
"documents": [{"name": "contract.pdf", "content": pdf}],
},
)
created = response.json()["case"]
print(created["recipients"][0]["signingUrl"])
```
You get back `201` with the whole case, including a signing link per recipient. This is the complete body — nothing is elided:
```json title="201 Created"
{
"case": {
"id": "65ab12cd34ef56ab78cd90b0",
"referenceId": null,
"title": "Rental agreement",
"state": "open",
"language": "da",
"createdAt": "2026-07-27T14:48:36.356Z",
"expiresAt": "2026-08-26T14:48:36.356Z",
"folder": { "id": "65ab12cd34ef56ab78cd90a2", "name": "Default" },
"sender": {
"id": "65ab12cd34ef56ab78cd90ef",
"name": "Anna Beck",
"email": "anna@example.com"
},
"activeGroup": 0,
"recipients": [
{
"id": "65ab12cd34ef56ab78cd91a1",
"uid": "4t5ZQbtB",
"name": "Andreas Lauridsen",
"email": "al@example.com",
"phone": "",
"position": "",
"group": 0,
"type": "email",
"signing": true,
"status": "pending",
"needsCpr": false,
"signingSchema": "",
"emailStatus": null,
"signingUrl": "https://www.nextsign.dk/sign/65ab12cd34ef56ab78cd90b0/2/h6gKDYSiPyYTC5Bkq6urKfKKW"
}
],
"documents": [
{
"id": "65ab12cd34ef56ab78cd92c3",
"name": "contract.pdf",
"type": "application/pdf",
"url": "https://nextsign-de.fsn1.your-objectstorage.com/65ab/contract.pdf",
"signObligated": true,
"documentMustBeRead": false,
"signatories": []
}
],
"delivery": {
"attempted": true,
"email": { "ok": true },
"eboks": { "ok": true },
"sms": { "ok": true },
"webhook": { "ok": true }
},
"notices": [
{
"code": "sender-defaulted-to-key-creator",
"message": "Neither senderId nor senderEmail was provided. The case was attributed to the API key's creator (anna@example.com)."
},
{
"code": "default-signing-schemas",
"message": "No settings.signingSchemas provided — the standard eIDs were used.",
"using": [
"urn:grn:authn:dk:mitid:low",
"urn:grn:authn:dk:mitid:substantial",
"urn:grn:authn:dk:mitid:business",
"urn:grn:authn:se:bankid",
"urn:grn:authn:de:personalausweis"
]
}
]
}
}
```
The two `notices` are there because the request left something out. Neither is a problem — each one tells you what was chosen on your behalf:
- **`sender-defaulted-to-key-creator`** — no `senderId` or `senderEmail` was sent, so the case is owned by whoever created the API key. See [Sender](#sender)
- **`default-signing-schemas`** — no `settings.signingSchemas` were sent, so the standard eIDs apply. The `using` array lists exactly which ones
Everything else on this page is optional refinement of that request.
**Unknown fields are rejected.** If a field is not in the tables below, sending it returns `400` with `unknown field`. This is deliberate: a field name we quietly ignored would leave you with a case that behaves wrongly and no clue why.
## Query Parameters
One parameter, and it shapes the response rather than the case:
| Parameter | Type | Default | Description |
| --------- | ---- | ------- | ----------- |
| `publicUrls` | `boolean` | `false` | Return downloadable document links. See [Document URLs](#document-urls) |
It is a query parameter rather than a body field because nothing about it is stored — the same body always creates the same case, whatever you ask the response to look like.
## Request Body
| Field | Type | Required | Default | Description |
| ----- | ---- | -------- | ------- | ----------- |
| `title` | `string` | **Yes** | — | Case title. Max 300 characters |
| `recipients` | `array` | Conditional | — | Max 100. **Required when `state` is `open`** |
| `documents` | `array` | Conditional | — | Max 20. **Required when `state` is `open`** |
| `state` | `string` | No | `open` | `open` sends the case, `draft` saves it for later editing |
| `referenceId` | `string` | No | — | Your own reference. Max 200 characters |
| `folder` | `string` | No | company default | Folder **name or id**. Must exist — see [Folders](#folders) |
| `settings` | `object` | No | — | How the case behaves — language, expiry, reminders, signing methods. See [Settings](#settings) |
| `messageTemplateId` | `string` | No | — | Template for the message recipients receive |
| `presetId` | `string` | No | — | Preset id. See [Presets](#presets) |
| `senderId` | `string` | No | the key's creator | Attribute the case to a colleague, by user id. See [Sender](#sender) |
| `senderEmail` | `string` | No | the key's creator | Attribute the case to a colleague, by email. See [Sender](#sender) |
| `tags` | `array` | No | — | Tag values merged into `.docx` documents. Max 500 |
A `draft` may legitimately be incomplete — no recipients, no documents — exactly like a case started in the dashboard and finished later. An `open` case is going out, so both are required.
---
## Settings
Everything that shapes how the case behaves — as opposed to what it contains and who it involves — lives in the `settings` object:
```json
{
"settings": {
"language": "da",
"expiresInDays": 14,
"reminders": { "send": true, "amount": 2, "daysBetween": 3 }
}
}
```
| Field | Type | Required | Default | Description |
| ----- | ---- | -------- | ------- | ----------- |
| `autoSend` | `boolean` | No | `true` | On an `open` case, whether to send it now. `false` creates it ready to send |
| `language` | `string` | No | company language | Signing page language. See [Languages](#languages) |
| `template` | `number` | No | company default | Signing page template number, `1`–`99` |
| `expiresInDays` | `number` | No | `30` | Days until the case expires, `1`–`3650` |
| `message` | `string` | No | template message | Custom message to recipients. Max 5000 characters |
| `attachSignedFiles` | `boolean` | No | `true` | Attach signed PDFs to the completion email |
| `signingSchemas` | `array` | No | standard eIDs | Allowed signing methods. Each recipient only sees the ones that fit their `needsCpr`. See [Signing Schemas](#signing-schemas) |
| `reminders` | `object` | No | — | Reminder policy. See [Reminders](#reminders) |
| `logo` | `string` | No | — | Signing page logo. See [Logo](#logo) |
| `integrations` | `object` | No | — | Integration hooks. See [Integrations](#integrations) |
Every field is optional, and so is the object itself — omit `settings` entirely and the case follows your company's defaults.
---
## Recipients
| Field | Type | Required | Default | Description |
| ----- | ---- | -------- | ------- | ----------- |
| `name` | `string` | **Yes** | — | Max 200 characters |
| `email` | `string` | Conditional | — | Required when `type` is `email` (the default). Not needed for `sms` or `eboks` recipients |
| `phone` | `string` | Conditional | — | Required when `type` is `sms`. Max 40 characters |
| `type` | `string` | No | `email` | `email`, `sms`, or `eboks` |
| `signing` | `boolean` | No | `true` | Whether this recipient must sign. `false` makes them a read-only observer |
| `group` | `number` | No | `0` | Signing order, `0`–`100`. Lower groups are contacted first |
| `position` | `string` | No | — | Signature title. Max 200 characters |
| `needsCpr` | `boolean` | No | `false` | Require the signer's identity to match `cpr`. Also decides which signing methods they are offered, see [Signing Schemas](#signing-schemas) |
| `cpr` | `string` | Conditional | — | Required when `needsCpr` is `true`. On an `eboks` recipient it is the e-Boks address: a 10-digit CPR or an 8-digit CVR, required unless `eboks.cvr` or `eboks.address` is given. Max 20 characters |
| `signingSchema` | `string` | No | — | Lock this recipient to one signing method |
| `redirectUrl` | `string` | No | — | Where to send the recipient after signing |
| `message` | `string` | No | — | Per-recipient message. Max 2000 characters |
| `messageTemplateId` | `string` | No | — | Override the case template for this recipient. An unknown id fails the request with `message-template-not-found` |
| `eboks` | `object` | No | — | e-Boks delivery payload. See the [e-Boks Object](https://docs.nextsign.dk/docs/objects#e-boks-object) |
**`signing` defaults to `true`.** Everyone you list is a signer unless you say otherwise, so set `"signing": false` explicitly on anyone who should only receive a copy.
**Sequential signing.** Recipients in group `0` are contacted first; group `1` is contacted only once every signer in group `0` has signed, and so on. Recipients with the same group are contacted together.
**SMS text.** An `sms` recipient receives, in order of priority: their own `message`, the case's `settings.message`, or the body of the template given by `messageTemplateId` (on the recipient, then on the case). A template is not required: without any of these, they get a short standard invitation in the case language, worded for signing, approving or reading. The signing link is added to any SMS text that does not contain `{recipient_url}`.
**SMS sender.** The SMS shows the sender name of that same template (`messageTemplateId` on the recipient, then on the case), even when the text comes from `message`. Without a template it is sent from your company name, cut to its first word. To send from your own sender name, set `messageTemplateId` to your SMS template on the `sms` recipient. A sender name is at most 11 characters; stick to letters and digits.
If `needsCpr` is `true` you must also send `cpr`, or the signer cannot complete verification. Store the plain-text CPR on your side if you need it later — it is encrypted at rest here and is not returned by the API.
---
## Documents
Each document needs a `name` and **exactly one source**: `url`, `content`, or `documentId`. Sending two sources is a `400` — accepting several and picking a winner is how a caller ends up sending a different file than they thought.
| Field | Type | Required | Default | Description |
| ----- | ---- | -------- | ------- | ----------- |
| `name` | `string` | **Yes** | — | File name including extension. Max 300 characters |
| `url` | `string` | One of | — | An `http(s)` URL. Stored by reference — see [File sources](#file-sources) |
| `content` | `string` | One of | — | Base64 file data. Max **20 MB decoded** |
| `documentId` | `string` | One of | — | A document from your NextSign library. See [Library documents](#library-documents) |
| `versionId` | `string` | No | current version | Pin a specific library document version |
| `tags` | `object` | No | — | Values for a `.docx` library document's fields, keyed by field path. Checked against the document — see [Library documents](#library-documents) |
| `signObligated` | `boolean` | No | `true` | Whether signing this document is required |
| `documentMustBeRead` | `boolean` | No | `false` | Require the recipient to open it before signing |
| `signatories` | `array` | No | all | Indexes into `recipients` — who signs this document |
`signatories` are **positions in your `recipients` array**, so `[0, 2]` means the first and third recipient. An index outside the array is a `400`, not a silent drop.
### File sources
The three sources are not checked the same way, and the difference matters.
**`content` is inspected.** It must decode to a **PDF** or a **`.docx`**; the bytes decide, and the file name and any declared type are ignored. Anything else is a `400`.
This rejects things a four-byte check waves through. A `.docx` is a ZIP container, so an XLSX, a PPTX or a plain `.zip` renamed to `.docx` all share the same prefix. Storing one of those would break the signing page later, a long way from the cause.
**The stored file name follows the content.** Upload PDF bytes named `contract.docx` and the document is stored and displayed as `contract.pdf`.
**`url` is stored by reference.** We do not download it while creating the case, so its contents are not inspected and a URL that 404s is still accepted — you will find out at signing time, not at creation time. Make sure it resolves, and stays resolving, for as long as the case is open.
The one exception: a `url` whose **`name` ends in `.docx`** is fetched immediately and converted, because the signing page cannot render Word files. Here the name decides, not the bytes — so a URL holding a PDF but named `.docx` is fetched, fails to convert, and comes back as `document-conversion-failed`.
**`documentId` comes from your library**, and is produced and converted the same way the dashboard does it — see [Library documents](#library-documents).
A `.docx` you supply as `content` or `url` is converted to PDF before it is attached. If the case has a top-level `tags` array, those values are merged into it during that conversion.
### Library documents
A `documentId` attaches a document from **Dashboard → Documents**. Find the id with [List Documents](https://docs.nextsign.dk/docs/v3/documents-list); the document must be one your key can see — the company's non-private documents, the ones the key's user created, and documents shared with you — or it is `document-not-found`.
A `.docx` in the library is a template, and `documents[].tags` fills its fields. The keys are the field paths [Retrieve a Document](https://docs.nextsign.dk/docs/v3/documents-get) lists, and the values are **checked against the document before anything is created**: unknown keys, values for automatic (global, hidden or computed) fields, wrong types, values outside the author's bounds, and missing required fields all come back as `validation-failed`, with paths like `documents[0].tags.start_date`. A `pdf` document has no fields, so any `tags` key for it is rejected. The rules are the same as on [Create a Case from a Document](https://docs.nextsign.dk/docs/v3/documents-create-case#tags), which is the simpler endpoint when the case is built around one template.
`name` is still required on a library document, but the attached file keeps the library version's own name — a `.docx` becomes `.pdf` once rendered. On a `draft` the document is attached by reference with the values still editable in the dashboard, and rendered when the draft is sent.
### Files over 20 MB
Upload the file first with [Document Upload](https://docs.nextsign.dk/docs/documents-upload), then pass the returned URL as `url`:
```json
{
"documents": [
{ "name": "Lejekontrakt.pdf", "url": "https://nextsign-dev.hel1.your-objectstorage.com/.../Lejekontrakt.pdf" }
]
}
```
---
## Sender
The sender **owns** the case. That decides two real things: which colleagues can see it — through the folder's role list — and who the signing email says it came from.
Name one in either of two ways:
| Field | Looks the user up by |
| ----- | -------------------- |
| `senderId` | Their NextSign user id |
| `senderEmail` | Their email address, matched without case sensitivity |
```json
{ "senderId": "65ab12cd34ef56ab78cd90ef" }
```
Send both and `senderId` wins — an id identifies a user exactly, where an address is only a way of looking one up.
The user must be a member of your company. Anyone else — including a valid user id from another company — is rejected:
```json title="400 Bad Request"
{
"error": "sender-user-not-found",
"details": [{ "field": "senderId", "message": "not a member of this company" }]
}
```
**The display name and email come from that user's NextSign profile**, not from your payload. You cannot set them per request, so a case can never claim to have been sent by an address your company does not own.
### If you send neither
The case is attributed to whoever created the API key, and the response tells you so:
```json
{
"code": "sender-defaulted-to-key-creator",
"message": "Neither senderId nor senderEmail was provided. The case was attributed to the API key's creator (anna@example.com)."
}
```
That is a sensible default for a key that belongs to one person. If a single key serves a whole team, name the sender on every request — otherwise every case is owned by, and appears to come from, the colleague who happened to create the key.
---
## Folders
`folder` accepts a folder **name or id**. It must match a folder that exists:
```json title="400 Bad Request"
{
"error": "invalid-folder",
"details": [{ "field": "folder", "message": "no folder named or with id 'Contracts'" }]
}
```
A misspelled folder name is rejected rather than quietly filed under the default one. The folder controls which roles can see the case, so falling back silently would put it in front of a different set of colleagues than you intended.
The case's role visibility always comes from the folder.
---
## Presets
`presetId` applies your company's preset — folder, message template, language, signing schemas, reminders, expiry and file attachment policy.
A preset fills in whatever you did not send. If the preset has **locked** a field, it overrides the value you sent, because presets are how a company enforces signing policy.
`settings.reminders.autoSend` is never locked by a preset — a preset governs whether reminders happen, not which system dispatches them.
---
## Reminders
`settings.reminders` is the reminder policy for the case:
| Field | Type | Range | Default | Description |
| ----- | ---- | ----- | ------- | ----------- |
| `send` | `boolean` | — | `true` | Master switch. `false` means no reminders at all |
| `amount` | `number` | `1`–`20` | `2` | How many reminders each recipient receives |
| `daysBetween` | `number` | `1`–`365` | `3` | Days between reminders |
| `autoSend` | `boolean` | — | `true` | Whether **we** send them |
Fields are merged individually, so `{"reminders": {"daysBetween": 10}}` keeps the default `send` and `amount`.
`autoSend: false` is the seam for sending reminders yourself. The reminder run still happens on schedule and still fires the `case.reminder` and `case.reminder.sent` [webhooks](https://docs.nextsign.dk/docs/webhooks) — it just does not send the email or SMS. Use `send: false` if you want no reminder activity at all.
---
## Logo
Puts your own logo on the signing page, for this case only.
```json
{ "settings": { "logo": "iVBORw0KGgoAAAANSUhEUgAA..." } }
```
`settings.logo` accepts either **base64 PNG or JPEG data**, or a logo URL previously returned by NextSign. Base64 data is validated by content and stored for you; the case then carries the hosted URL. Maximum 8 MB of base64, which is roughly a 6 MB image.
Arbitrary third-party URLs are rejected — a signing page must not depend on a resource we do not control, which can change or disappear long after the case is signed.
If the upload itself fails, the case is still created and you get a `signing-page-logo-not-stored` notice rather than an error. A logo is decoration; losing it is not worth failing a signing case over.
---
## Integrations
```json
{ "settings": { "integrations": { "microsoft": { "returnPath": "/sites/MySite/Shared Documents" } } } }
```
`returnPath` is where signed documents are written back in SharePoint, when your company has an active SharePoint integration. It must be a **site-relative path beginning with `/`**. If there is no active integration the field is ignored and you get a notice rather than an error.
---
## Signing Schemas
Case-level `settings.signingSchemas` lists the methods recipients may choose from. Omit it and the standard eIDs are used.
**`low` and `substantial` are not security levels.** Both are MitID Privat: `urn:grn:authn:dk:mitid:low` is the login *without* a CPR match, `urn:grn:authn:dk:mitid:substantial` is the login *with* a CPR match. A recipient is only offered the methods that fit their own `needsCpr`, so `substantial` is never shown to a recipient with `needsCpr: false`. If you want MitID Privat, send both values and NextSign picks the right one for each recipient.
| Value | Method | Offered to recipients with | In the default set |
| ----- | ------ | -------------------------- | ------------------ |
| `urn:grn:authn:dk:mitid:low` | MitID Privat | `needsCpr: false` | Yes |
| `urn:grn:authn:dk:mitid:substantial` | MitID Privat with CPR match | `needsCpr: true` | Yes |
| `urn:grn:authn:dk:mitid:business` | MitID Erhverv | `needsCpr: false` | Yes |
| `urn:grn:authn:se:bankid` | Swedish BankID | `needsCpr: false` | Yes |
| `urn:grn:authn:de:personalausweis` | German Personalausweis | `needsCpr: false` | Yes |
| `draw` | Drawn signature | `needsCpr: false` | No |
| `draw-cpr` | Drawn signature with CPR | `needsCpr: true` | No |
| `sms-email` | SMS + email verification | `needsCpr: false` | No |
### Which methods a recipient sees
Each recipient gets the intersection of the case-level list and the methods for their `needsCpr`:
- **`needsCpr: false`**: every method in your list except `substantial` and `draw-cpr`.
- **`needsCpr: true`**: only `substantial` and `draw-cpr`. MitID Erhverv, BankID and Personalausweis are not offered to a recipient who needs a CPR match.
A common mistake is sending only the CPR variant of MitID Privat to recipients who do not need a CPR match:
| `settings.signingSchemas` | A recipient with `needsCpr: false` sees |
| ------------------------- | --------------------------------------- |
| `["urn:grn:authn:dk:mitid:substantial", "urn:grn:authn:dk:mitid:business"]` | MitID Erhverv only |
| `["urn:grn:authn:dk:mitid:low", "urn:grn:authn:dk:mitid:substantial", "urn:grn:authn:dk:mitid:business"]` | MitID Privat and MitID Erhverv |
If none of your methods fits a recipient, that recipient has no way to sign. Check the list against every recipient's `needsCpr` before you send.
### Locking a recipient to one method
A recipient's own `signingSchema` locks that recipient to one method, whether or not it is in the case-level list. For methods that come in two variants (MitID Privat and drawn signature), NextSign uses the variant that matches the recipient's `needsCpr`, so a locked `low` becomes `substantial` when `needsCpr` is `true`, and the other way round. If a recipient locked to any other method has `needsCpr: true`, CPR verification runs through MitID Privat first and the signer then continues with the locked method.
## Languages
| Value | Language |
| ----- | -------- |
| `da` | Danish |
| `en` | English |
| `de` | German |
| `se` | Swedish |
| `no` | Norwegian |
Omit `settings.language` and the case follows your company's default.
---
## Full Example
```bash title="cURL"
curl --location 'https://api.nextsign.dk/v3/api/cases' \
-H "Authorization: Bearer ${NEXTSIGN_API_KEY}" \
-H 'Content-Type: application/json' \
--data-raw '{
"title": "Lejeaftale",
"referenceId": "Jrn. 2342-23",
"folder": "Default",
"senderEmail": "anna@example.com",
"settings": {
"language": "da",
"expiresInDays": 14,
"message": "Kære {recipient_name}, som aftalt er hermed dokumenter til underskrift",
"reminders": { "send": true, "amount": 2, "daysBetween": 3 },
"signingSchemas": [
"urn:grn:authn:dk:mitid:low",
"urn:grn:authn:dk:mitid:substantial",
"urn:grn:authn:dk:mitid:business"
]
},
"recipients": [
{
"name": "Andreas Lauridsen",
"email": "al@example.com",
"group": 0,
"position": "Director",
"redirectUrl": "https://example.com/complete"
},
{
"name": "Mia Sørensen",
"email": "mia@example.com",
"group": 1
}
],
"documents": [
{
"name": "Lejekontrakt.pdf",
"content": "JVBERi0xLjQKJeLjz9M...",
"signObligated": true,
"signatories": [0, 1]
}
]
}'
```
```json title="201 Created"
{
"case": {
"id": "65ab12cd34ef56ab78cd90b0",
"referenceId": "Jrn. 2342-23",
"title": "Lejeaftale",
"state": "open",
"language": "da",
"createdAt": "2026-07-27T14:48:36.356Z",
"expiresAt": "2026-08-10T14:48:36.356Z",
"folder": { "id": "65ab12cd34ef56ab78cd90a2", "name": "Default" },
"sender": {
"id": "65ab12cd34ef56ab78cd90ef",
"name": "Anna Beck",
"email": "anna@example.com"
},
"activeGroup": 0,
"recipients": [
{
"id": "65ab12cd34ef56ab78cd91a1",
"uid": "4t5ZQbtB",
"name": "Andreas Lauridsen",
"email": "al@example.com",
"phone": "",
"position": "Director",
"group": 0,
"type": "email",
"signing": true,
"status": "pending",
"needsCpr": false,
"signingSchema": "",
"emailStatus": null,
"signingUrl": "https://www.nextsign.dk/sign/65ab12cd34ef56ab78cd90b0/2/h6gKDYSiPyYTC5Bkq6urKfKKW"
},
{
"id": "65ab12cd34ef56ab78cd91a2",
"uid": "9kPmR2xW",
"name": "Mia Sørensen",
"email": "mia@example.com",
"phone": "",
"position": "",
"group": 1,
"type": "email",
"signing": true,
"status": "pending",
"needsCpr": false,
"signingSchema": "",
"emailStatus": null,
"signingUrl": "https://www.nextsign.dk/sign/65ab12cd34ef56ab78cd90b0/2/Rr8tVqLmXnPzB4dYw2sKfHjNe"
}
],
"documents": [
{
"id": "65ab12cd34ef56ab78cd92c3",
"name": "Lejekontrakt.pdf",
"type": "application/pdf",
"url": "https://nextsign-de.fsn1.your-objectstorage.com/65ab/Lejekontrakt.pdf",
"signObligated": true,
"documentMustBeRead": false,
"signatories": ["4t5ZQbtB", "9kPmR2xW"]
}
],
"delivery": {
"attempted": true,
"email": { "ok": true },
"eboks": { "ok": true },
"sms": { "ok": true },
"webhook": { "ok": true }
},
"notices": []
}
}
```
The `Location` response header carries the case path.
## Response fields
| Field | Description |
| ----- | ----------- |
| `id` | The case id. Store it — it identifies the case in the dashboard and in every webhook |
| `sender` | The resolved owner — id, name and email from their NextSign profile |
| `activeGroup` | The recipient group currently being asked to sign |
| `recipients[].id` | The recipient's id |
| `recipients[].uid` | Short handle for the recipient. This is what `documents[].signatories` contains |
| `documents[].id` | The document's id, assigned when the case is created |
| `documents[].signatories` | Who signs this document, as `recipients[].uid` values |
| `recipients[].status` | `pending`, `signed`, `denied`, or `not signing`. This is the signing status, not the email's |
| `recipients[].emailStatus` | Delivery of the case emails to this recipient. Always `null` here, because nothing has been reported yet when the case is created. Read it later with [Retrieve a Case](https://docs.nextsign.dk/docs/v3/case-get#email-status) |
| `recipients[].signingUrl` | The signing link. Present only on `open` cases |
| `documents[].type` | The file's MIME type, e.g. `application/pdf`. `null` for a document you supplied as a `url` |
| `documents[].url` | The document's stored address. Protected — add `?publicUrls=true` for a downloadable link. See [Document URLs](#document-urls) |
| `delivery` | Per-channel send result. See [Delivery](#delivery) |
| `notices` | Non-fatal remarks. Never an error |
`signingToken` is deliberately not returned. It is the credential that authenticates a signing session, and `signingUrl` already contains everything you need. Building signing URLs by hand breaks when the signing domain, language prefix or template number changes — all three are resolved server-side per case.
### Delivery
`delivery` reports each channel independently:
```json
{ "attempted": true, "email": { "ok": false, "error": "ECONNREFUSED" }, "eboks": { "ok": true }, "sms": { "ok": true }, "webhook": { "ok": true } }
```
- `attempted: false` — nothing was sent. Either the case is a `draft`, or `settings.autoSend` was `false`. The key is still present, so read `attempted` rather than testing whether `delivery` exists
- `webhook` — appears on every `open` case, including one created with `autoSend: false`. Your integration is told the case exists even when we did not mail anyone
- `ok: false` — that channel failed. **The case still exists**, and the response is still `201`
`eboks` also reports each e-Boks recipient, so a rejected delivery (a CPR not registered for Digital Post, an address that cannot be resolved) comes back in this same response:
```json
{
"ok": false,
"error": "eboks-delivery-failed",
"recipients": [
{ "recipientId": "66263484e601d81006d1a133", "name": "Andreas Lauridsen", "ok": false, "error": "invalid-address" },
{ "recipientId": "66263484e601d81006d1a134", "name": "Anna Jensen", "ok": true, "medium": "eboks" }
]
}
```
`ok` is `false` when any e-Boks recipient failed. `email` reports `ok: false` (`error: "email-delivery-failed"`) when the invitation email could not be sent.
An `eboks` recipient is reached through e-Boks only: an `email` or `phone` on it is not used for an invitation, so it never receives the case twice. `medium` is `eboks`, or `letter` when the delivery fell back to a physical letter. The failure also fires the [`case.delivery.failed` webhook](https://docs.nextsign.dk/docs/webhooks#delivery-failed-payload), which is where you learn about e-Boks failures on sends that happen later, such as reminders. A case without e-Boks recipients reports `"eboks": { "ok": true }`.
A failed channel is never reported as a failed request. Returning an error for a case that exists — and may already have been emailed — is what makes an integrator retry into duplicates.
### Notices
```json
{ "code": "sender-defaulted-to-key-creator", "message": "Neither senderId nor senderEmail was provided. …" }
```
Notices tell you something was defaulted, ignored or degraded. They never mean the request failed. Log them; do not branch on them. The full list is in [Errors](https://docs.nextsign.dk/docs/v3/errors#notices-are-not-errors).
### Document URLs
`documents[].url` is the document's **stored** address. It identifies the file and is stable, but it is protected — fetching it without credentials returns `403`.
Add `?publicUrls=true` for **downloadable** links, valid for one hour:
```bash title="cURL"
curl "https://api.nextsign.dk/v3/api/cases?publicUrls=true" \
-H "Authorization: Bearer $NEXTSIGN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Rental agreement", … }'
```
```json
{ "url": "https://nextsign-de.fsn1.your-objectstorage.com/65ab/contract.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-Signature=fa2ca5b25…" }
```
They are opt-in because they expire: a link minted at creation is dead an hour later, so storing one alongside the case leaves you with something that no longer works. The case id does not expire — ask for a fresh link with [Retrieve a Case](https://docs.nextsign.dk/docs/v3/case-get) when you actually need to download.
Documents you supplied as a `url` are returned exactly as you gave them, with or without this parameter. We store those by reference and never re-host them, which is also why their `type` is `null` — we never fetched the bytes to inspect.
A `.docx` you upload is converted to PDF on the way in, so its `name` comes back as `.pdf` and its `type` as `application/pdf`. Both describe the file at `url`, not what you sent.
---
## Errors
```json title="400 Bad Request"
{
"error": "validation-failed",
"details": [
{ "field": "title", "message": "is required" },
{ "field": "recipients[0].email", "message": "must be a valid email" },
{ "field": "documents[0]", "message": "requires one of: url, content, documentId" },
{ "field": "nmae", "message": "unknown field" }
]
}
```
Structural problems in the body come back at once, so you fix them in one round-trip rather than one per deploy. A document's `content` is checked for being a real PDF or `.docx` once the structure is valid — see [Validation returns everything at once](https://docs.nextsign.dk/docs/v3/errors#validation-returns-everything-at-once).
| `error` | Meaning |
| ------- | ------- |
| `validation-failed` | See `details` |
| `invalid-folder` | No folder with that name or id |
| `message-template-not-found` | The case's `messageTemplateId`, or a recipient's, was not found. `details` names each one, e.g. `recipients[1].messageTemplateId` |
| `preset-not-found` | Not found for your company |
| `sender-user-not-found` | `senderId` or `senderEmail` is not a member of your company |
| `invalid-signing-page-logo` | `settings.logo` is not a PNG/JPEG, and not a NextSign URL |
| `document-not-found` | `documentId` is not in your library, or not visible to this key |
| `document-version-not-found` | `versionId` does not belong to that document |
| `document-conversion-failed` | The `.docx` could not be converted — usually a corrupt file |
| `document-production-failed` | A library document could not be produced |
**Every `4xx` from this endpoint means nothing was created.** The case is persisted only after all of these have passed, so a `4xx` is always safe to fix and resend. Full detail in [Errors](https://docs.nextsign.dk/docs/v3/errors).
## Related
### List Cases
Source: https://docs.nextsign.dk/docs/v3/case-list
Summary: Page through your company's cases with GET /v3/api/cases — filters, cursor pagination and the response shape.
Returns your company's cases, newest first, one page at a time.
```text
GET https://api.nextsign.dk/v3/api/cases
```
Authenticated with an API key — see [Authorization](https://docs.nextsign.dk/docs/v3/authorization).
## Quickstart
```bash
curl "https://api.nextsign.dk/v3/api/cases?limit=25&state=open" \
-H "Authorization: Bearer $NEXTSIGN_API_KEY"
```
```js
const params = new URLSearchParams({ limit: '25', state: 'open' });
const response = await fetch(`https://api.nextsign.dk/v3/api/cases?${params}`, {
headers: { Authorization: `Bearer ${process.env.NEXTSIGN_API_KEY}` },
});
const { cases, nextCursor, hasMore } = await response.json();
```
```python
import os, requests
response = requests.get(
"https://api.nextsign.dk/v3/api/cases",
headers={"Authorization": f"Bearer {os.environ['NEXTSIGN_API_KEY']}"},
params={"limit": 25, "state": "open"},
)
page = response.json()
```
---
## Query Parameters
| Parameter | Type | Default | Description |
| --------- | ---- | ------- | ----------- |
| `limit` | `integer` | `25` | Cases per page, 1–100 |
| `cursor` | `string` | — | Where to continue from. Use `nextCursor` from the previous page |
| `state` | `string` | all | One of `draft`, `open`, `completed`, `denied`, `cancelled` |
| `referenceId` | `string` | all | Exact match on your own identifier. Not a search |
| `folder` | `string` | all | Exact folder **name** |
| `createdAfter` | `string` | — | ISO 8601. Cases created at or after this moment |
| `createdBefore` | `string` | — | ISO 8601. Cases created at or before this moment |
| `includeDeleted` | `boolean` | `false` | Include cases moved to the bin |
| `publicUrls` | `boolean` | `false` | Return downloadable document links. See [Document URLs](#document-urls) |
Anything else is rejected. A misspelled parameter returns `400 validation-failed` naming it, rather than silently returning unfiltered results — a filter that is quietly ignored is the one mistake you cannot see in the response.
```json title="400 Bad Request"
{
"error": "validation-failed",
"details": [{ "field": "page", "message": "unknown field" }]
}
```
**Dates must be ISO 8601** — `2026-01-31` or `2026-01-31T09:00:00Z`. A bare year like `2026` is rejected rather than quietly read as January 1st.
---
## Pagination
Paging is by cursor, not page number. Each response carries a `nextCursor`; pass it back to get the next page, and stop when `hasMore` is `false`.
```js
let cursor = null;
const all = [];
do {
const params = new URLSearchParams({ limit: '100' });
if (cursor) params.set('cursor', cursor);
const response = await fetch(`https://api.nextsign.dk/v3/api/cases?${params}`, {
headers: { Authorization: `Bearer ${process.env.NEXTSIGN_API_KEY}` },
});
const page = await response.json();
all.push(...page.cases);
cursor = page.nextCursor;
} while (cursor);
```
Two things follow from this that page numbers would not give you:
- **New cases do not shift your pages.** A cursor marks a position in the results, not an offset, so a case created while you are paging cannot push a row onto a page you have already read — with `?page=` it would, and you would see the same case twice.
- **The last page costs what the first one did.** Every page is a bounded lookup rather than a count-and-skip, so paging deep into a long history does not get slower.
**Treat the cursor as opaque.** It is a position marker whose format is ours to change — do not parse it, build one, or store one long-term. A cursor you did not receive from us returns `400 validation-failed` on the `cursor` field.
There is no `total`. Counting every case in a company is the one query that would make deep paging expensive again, so we do not run it on your behalf. If you need a count, page with `limit=100` and add up what you receive.
---
## Document URLs
By default `documents[].url` is the document's **stored** address. It identifies the file and is stable, but it is protected — fetching it without credentials returns `403`.
Pass `?publicUrls=true` to get **downloadable** links instead:
```bash
curl "https://api.nextsign.dk/v3/api/cases?publicUrls=true" \
-H "Authorization: Bearer $NEXTSIGN_API_KEY"
```
```json
{ "url": "https://nextsign-de.fsn1.your-objectstorage.com/65ab/contract.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-Signature=fa2ca5b25…" }
```
These expire after an hour, which is why they are opt-in: minting one for every document on every page — when most callers are checking who has signed — produces links that are stale before anyone clicks them. Ask for them on the page where you intend to download.
Documents you supplied as a `url` are returned exactly as you gave them, with or without this parameter. We store those by reference and never re-host them — which is also why their `type` is `null`, since we never fetched the bytes to inspect.
---
## Response
```json title="200 OK"
{
"cases": [
{
"id": "65ab12cd34ef56ab78cd90b0",
"referenceId": "Jrn. 2342-23",
"title": "Lejeaftale",
"state": "open",
"language": "da",
"createdAt": "2026-07-27T14:48:36.356Z",
"expiresAt": "2026-08-10T14:48:36.356Z",
"folder": { "id": "65ab12cd34ef56ab78cd90a2", "name": "Default" },
"sender": {
"id": "65ab12cd34ef56ab78cd90ef",
"name": "Anna Beck",
"email": "anna@example.com"
},
"activeGroup": 0,
"recipients": [
{
"id": "65ab12cd34ef56ab78cd91a1",
"uid": "4t5ZQbtB",
"name": "Andreas Lauridsen",
"email": "al@example.com",
"phone": "",
"position": "",
"group": 0,
"type": "email",
"signing": true,
"status": "signed",
"needsCpr": false,
"signingSchema": "",
"emailStatus": {
"status": "opened",
"bounceType": null,
"sentAt": "2026-07-27T14:48:38.000Z",
"deliveredAt": "2026-07-27T14:48:41.000Z",
"openedAt": "2026-07-27T15:02:10.000Z",
"bouncedAt": null,
"updatedAt": "2026-07-27T15:02:10.000Z"
}
}
],
"documents": [
{
"id": "65ab12cd34ef56ab78cd92c3",
"name": "contract.pdf",
"type": "application/pdf",
"url": "https://nextsign-de.fsn1.your-objectstorage.com/65ab/contract.pdf",
"signObligated": true,
"documentMustBeRead": false,
"signatories": []
}
],
"notices": []
}
],
"nextCursor": "MTc4NTI0NDYwNjU5OS42NWFiMTJjZDM0ZWY1NmFiNzhjZDkwYjA",
"hasMore": true
}
```
Each entry is the same case object [Create a Case](https://docs.nextsign.dk/docs/v3/case-create#response-fields) returns, so anything you already parse from a creation response works here unchanged.
| Field | Description |
| ----- | ----------- |
| `cases` | This page of cases, newest first |
| `nextCursor` | Pass as `?cursor=` for the next page. `null` on the last page |
| `hasMore` | Whether another page exists. Equivalent to `nextCursor !== null` |
Two fields from the creation response are absent here, both because they describe an event rather than the case:
- **`delivery`** reports the outcome of sending, which happened once, at creation.
- **`recipients[].signingUrl`** is not returned by the list. Signing links contain the credential that authenticates a signing session, and a list is what integrations put on a schedule — one such request should not be able to collect every open case's signing link. Fetch the case with [Retrieve a Case](https://docs.nextsign.dk/docs/v3/case-get) when you need them.
`notices` is present and always empty. It records what we filled in for you while *creating* a case; nothing is being filled in here.
`recipients[].emailStatus` is included, so one list request tells you which invitations were delivered, opened or bounced across a whole page of cases. See [Email status](https://docs.nextsign.dk/docs/v3/case-get#email-status) for what each value means.
---
## What you see
An API key belongs to a company, and this endpoint returns that company's cases — all of them, regardless of which colleague created each one or which folder it sits in. Folder permissions govern what a person sees when they log in; they do not narrow what a key can read.
---
## Errors
| `error` | Status | Meaning |
| ------- | ------ | ------- |
| `validation-failed` | `400` | A query parameter is invalid or unrecognised. See `details` |
Reading never changes anything, so every request here is safe to retry. Full detail in [Errors](https://docs.nextsign.dk/docs/v3/errors).
## Related
### Retrieve a Case
Source: https://docs.nextsign.dk/docs/v3/case-get
Summary: Fetch one case by id with GET /v3/api/cases/{id}, including its signing links and document downloads.
Returns a single case — who has signed, what state it is in, whether the case emails arrived, and its signing links.
```text
GET https://api.nextsign.dk/v3/api/cases/{id}
```
Authenticated with an API key — see [Authorization](https://docs.nextsign.dk/docs/v3/authorization).
## Quickstart
```bash
curl https://api.nextsign.dk/v3/api/cases/65ab12cd34ef56ab78cd90b0 \
-H "Authorization: Bearer $NEXTSIGN_API_KEY"
```
```js
const response = await fetch(
`https://api.nextsign.dk/v3/api/cases/${caseId}`,
{ headers: { Authorization: `Bearer ${process.env.NEXTSIGN_API_KEY}` } },
);
const { case: found } = await response.json();
const signed = found.recipients.filter((r) => r.status === 'signed');
```
```python
import os, requests
response = requests.get(
f"https://api.nextsign.dk/v3/api/cases/{case_id}",
headers={"Authorization": f"Bearer {os.environ['NEXTSIGN_API_KEY']}"},
)
found = response.json()["case"]
```
The id is the `id` from the creation response. That response also returns a `Location` header pointing at this endpoint, so you can follow it directly.
---
## Path Parameter
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | `string` | The case id |
## Query Parameters
| Parameter | Type | Default | Description |
| --------- | ---- | ------- | ----------- |
| `publicUrls` | `boolean` | `false` | Return downloadable document links. See [Document URLs](#document-urls) |
| `includeDeleted` | `boolean` | `false` | Return the case even if it has been moved to the bin |
---
## Response
```json title="200 OK"
{
"case": {
"id": "65ab12cd34ef56ab78cd90b0",
"referenceId": "Jrn. 2342-23",
"title": "Lejeaftale",
"state": "open",
"language": "da",
"createdAt": "2026-07-27T14:48:36.356Z",
"expiresAt": "2026-08-10T14:48:36.356Z",
"folder": { "id": "65ab12cd34ef56ab78cd90a2", "name": "Default" },
"sender": {
"id": "65ab12cd34ef56ab78cd90ef",
"name": "Anna Beck",
"email": "anna@example.com"
},
"activeGroup": 0,
"recipients": [
{
"id": "65ab12cd34ef56ab78cd91a1",
"uid": "4t5ZQbtB",
"name": "Andreas Lauridsen",
"email": "al@example.com",
"phone": "",
"position": "",
"group": 0,
"type": "email",
"signing": true,
"status": "pending",
"needsCpr": false,
"signingSchema": "",
"emailStatus": {
"status": "delivered",
"bounceType": null,
"sentAt": "2026-07-27T14:48:38.000Z",
"deliveredAt": "2026-07-27T14:48:41.000Z",
"openedAt": null,
"bouncedAt": null,
"updatedAt": "2026-07-27T14:48:41.000Z"
},
"signingUrl": "https://www.nextsign.dk/sign/65ab12cd34ef56ab78cd90b0/2/h6gKDYSiPyYTC5Bkq6urKfKKW"
}
],
"documents": [
{
"id": "65ab12cd34ef56ab78cd92c3",
"name": "contract.pdf",
"type": "application/pdf",
"url": "https://nextsign-de.fsn1.your-objectstorage.com/65ab/contract.pdf",
"signObligated": true,
"documentMustBeRead": false,
"signatories": []
}
],
"notices": []
}
}
```
This is the same case object [Create a Case](https://docs.nextsign.dk/docs/v3/case-create#response-fields) returns, with one field missing: `delivery`, which reports the outcome of sending and therefore only exists on the response that did the sending.
This endpoint also reads back cases started in the dashboard, and those can be less filled in than one created here: a dashboard draft has no expiry until it is sent, so `expiresAt` can be `null` — and `activeGroup` is `null` whenever no group is currently being asked to sign, as on a draft.
`notices` is present and almost always empty — it mostly records what we filled in for you while *creating* a case. The one notice this endpoint can add is `signing-urls-unavailable`, when an open case's signing links could not be built; the case itself is still returned.
### Signing links
`recipients[].signingUrl` is returned for cases in the `open` state. A draft has nothing to sign yet, so the field is absent rather than `null`.
**Signing links are rebuilt, not remembered.** You do not need to store the URLs from the creation response — ask for the case again and they are regenerated. Build them by hand and they break, because the signing domain, language prefix and template number are all resolved per case and change over time.
**A signing link is a credential.** Anyone holding one can open that recipient's signing session, and for recipients who sign without an eID that is enough to sign as them. Treat these URLs like passwords: do not log them, and do not forward them anywhere the recipient would not expect. This is also why [List Cases](https://docs.nextsign.dk/docs/v3/case-list) does not return them.
---
## Email status
`recipients[].emailStatus` tells you whether the case emails, meaning the invitation and any reminders, reached the recipient. It is `null` until our email provider has reported something, and it stays `null` for recipients we do not email (`type` `sms` or `eboks`).
```json
"emailStatus": {
"status": "bounced",
"bounceType": "hard",
"sentAt": "2026-07-27T14:48:38.000Z",
"deliveredAt": null,
"openedAt": null,
"bouncedAt": "2026-07-27T14:48:40.000Z",
"updatedAt": "2026-07-27T14:48:40.000Z"
}
```
| Field | Description |
| ----- | ----------- |
| `status` | `sent`, `delivered`, `opened` or `bounced`. See below |
| `bounceType` | `hard` or `soft` when `status` is `bounced`, otherwise `null` |
| `sentAt` | When an email last left our servers |
| `deliveredAt` | When the recipient's mail server last accepted one |
| `openedAt` | When the recipient last opened one, or clicked a link in it |
| `bouncedAt` | When one last bounced. Can be set while `status` is not `bounced`, see below |
| `updatedAt` | The most recent event of any kind |
Every reminder is a new email with its own events, so each timestamp is the most recent event of its kind, and `null` if there has not been one.
`status` is the first of these that applies:
1. **`bounced`**: the newest delivery outcome is a bounce. If a later reminder is delivered, the status moves on to `delivered` or `opened`, while `bouncedAt` keeps the time of the bounce. A reminder that bounces after an earlier delivery makes it `bounced` again: the address stopped working.
2. **`opened`**: an email was opened, or a link in it clicked.
3. **`delivered`**: an email was accepted by the recipient's mail server.
4. **`sent`**: an email was sent and nothing more has been reported yet.
A **hard** bounce means the address does not exist, or the receiving server refused it permanently. A **soft** bounce is a temporary problem, such as a full mailbox or an unavailable server, that did not clear while our provider kept retrying. In both cases that email is not going to arrive, and the recipient needs a corrected address or another channel.
If you change a recipient's email address after sending, events reported for the old address no longer count.
**How fresh it is.** Events are written to the case as soon as our email provider reports them, usually within seconds for `sent` and `delivered`. You do not need to poll for bounces: subscribe to the [`case.delivery.failed` webhook](https://docs.nextsign.dk/docs/webhooks#delivery-failed-payload), which fires for every bounced case email.
**`opened` is a hint, not proof.** Opens are detected through a tracking image. Some mail clients load it automatically (Apple Mail with Mail Privacy Protection does), so an email can count as opened without being read. Others block images, so an email can be read without ever counting as opened. For whether the recipient actually acted, rely on `status`, the signing status.
---
## Document URLs
By default `documents[].url` is the document's **stored** address. It identifies the file and is stable, but it is protected — fetching it without credentials returns `403`.
Pass `?publicUrls=true` for a **downloadable** link, valid for one hour:
```bash
curl "https://api.nextsign.dk/v3/api/cases/65ab12cd34ef56ab78cd90b0?publicUrls=true" \
-H "Authorization: Bearer $NEXTSIGN_API_KEY"
```
Ask for it on the request where you intend to download, rather than storing the result — an hour-old link has already expired, while the case id never does.
Documents you supplied as a `url` are returned exactly as you gave them, with or without this parameter. We store those by reference and never re-host them — which is also why their `type` is `null`, since we never fetched the bytes to inspect.
---
## Errors
| `error` | Status | Meaning |
| ------- | ------ | ------- |
| `validation-failed` | `400` | The id is malformed, or a query parameter is invalid. See `details` |
| `case-not-found` | `404` | No such case in your company |
```json title="404 Not Found"
{ "error": "case-not-found" }
```
**`404` does not mean the case does not exist** — only that it is not yours to read. A case belonging to another company, a deleted case, and an id that was never issued all answer identically, on purpose: a response that told them apart would let anyone test whether a given case id exists in NextSign at all.
Reading never changes anything, so every request here is safe to retry. Full detail in [Errors](https://docs.nextsign.dk/docs/v3/errors).
## Related
## Documents
Browse your document library and create cases from your own templates.
### List Documents
Source: https://docs.nextsign.dk/docs/v3/documents-list
Summary: Page through your document library with GET /v3/api/documents — which documents your key can use as the source of a case.
Returns the documents in your library (**Dashboard → Documents**) that your key may use as the source of a signing case, newest first, one page at a time.
```text
GET https://api.nextsign.dk/v3/api/documents
```
Authenticated with an API key — see [Authorization](https://docs.nextsign.dk/docs/v3/authorization).
## Quickstart
```bash
curl "https://api.nextsign.dk/v3/api/documents?limit=25" \
-H "Authorization: Bearer $NEXTSIGN_API_KEY"
```
```js
const response = await fetch('https://api.nextsign.dk/v3/api/documents?limit=25', {
headers: { Authorization: `Bearer ${process.env.NEXTSIGN_API_KEY}` },
});
const { documents, nextCursor, hasMore } = await response.json();
const templates = documents.filter((d) => d.format === 'docx');
```
```python
import os, requests
response = requests.get(
"https://api.nextsign.dk/v3/api/documents",
headers={"Authorization": f"Bearer {os.environ['NEXTSIGN_API_KEY']}"},
params={"limit": 25},
)
page = response.json()
```
---
## Query Parameters
| Parameter | Type | Default | Description |
| --------- | ---- | ------- | ----------- |
| `limit` | `integer` | `25` | Documents per page, 1–100 |
| `cursor` | `string` | — | Where to continue from. Use `nextCursor` from the previous page |
Anything else is rejected with `400 validation-failed` naming the parameter — the same rule as everywhere else in v3.
There is no search or filter yet. The list is small for most companies; page through it and pick by `name` on your side.
---
## Pagination
Paging is by cursor, exactly as for [List Cases](https://docs.nextsign.dk/docs/v3/case-list#pagination): pass `nextCursor` back as `?cursor=` and stop when `hasMore` is `false`. Treat the cursor as opaque, and do not store it — a cursor you did not receive from us returns `400 validation-failed` on the `cursor` field.
---
## Response
```json title="200 OK"
{
"documents": [
{
"id": "6a58c2a06f829bebd9232c1b",
"name": "Employment Contract.docx",
"format": "docx",
"private": true,
"shared": false,
"createdAt": "2026-05-12T09:14:08.201Z",
"updatedAt": "2026-05-12T09:14:09.377Z",
"currentVersion": {
"id": "6a58c2a16f829bebd9232c1e",
"versionNumber": 1,
"name": "Employment Contract.docx",
"format": "docx",
"createdAt": "2026-05-12T09:14:09.301Z",
"fieldCount": 27
}
},
{
"id": "6a33dbb9426f03b090c79ea2",
"name": "Terms of service.pdf",
"format": "pdf",
"private": false,
"shared": false,
"createdAt": "2026-04-14T11:02:51.940Z",
"updatedAt": "2026-04-14T11:02:52.118Z",
"currentVersion": {
"id": "6a33dbb9426f03b090c79ea5",
"versionNumber": 1,
"name": "Terms of service.pdf",
"format": "pdf",
"createdAt": "2026-04-14T11:02:52.080Z",
"fieldCount": 0
}
}
],
"nextCursor": "MTc4MjE0MDYyNDEyOS42YTM5NGVkMDhlZThjMzY2OWYyYzNlMTc",
"hasMore": true
}
```
| Field | Description |
| ----- | ----------- |
| `id` | The document id. This is the `documentId` you pass when [creating a case](https://docs.nextsign.dk/docs/v3/documents-create-case) |
| `name` | The current version's file name |
| `format` | `docx` or `pdf`. Only a `docx` has fillable fields — see below |
| `private` | Whether the document is visible only to its creator in the dashboard |
| `shared` | `true` when the document belongs to another company that has shared it with yours |
| `currentVersion` | The version used unless you pin a `versionId`. `fieldCount` is how many fields a caller can fill |
| `nextCursor` | Pass as `?cursor=` for the next page. `null` on the last page |
| `hasMore` | Whether another page exists |
The list deliberately says nothing about the fields themselves beyond their count. Fetch the document with [Retrieve a Document](https://docs.nextsign.dk/docs/v3/documents-get) to learn what to fill — it is one request per document, against a schema that rarely changes, so cache it.
**`docx` versus `pdf` is decided by the stored file.** A `.docx` in the library is a template: it has `{tag}` placeholders that are filled with the values you send and then rendered to PDF when the case is created. A `.pdf` is attached as it is, and sending `tags` for it is rejected.
---
## What you see
An API key belongs to a company, and this endpoint returns that company's library as the person behind the key would see it in the dashboard:
- every document that is **not private**,
- every document **created by the key's user** (keys made in the dashboard carry the user who made them),
- every document **another company has shared** with yours — these come back with `shared: true`.
A private document created by a colleague is not listed, and asking for it by id returns `404`, the same as the dashboard would hide it. A key with no user behind it sees only the non-private and shared documents.
Documents that exist only inside a public form are never listed — they are part of the form, not of the library.
---
## Errors
| `error` | Status | Meaning |
| ------- | ------ | ------- |
| `validation-failed` | `400` | A query parameter is invalid or unrecognised. See `details` |
Reading never changes anything, so every request here is safe to retry. Full detail in [Errors](https://docs.nextsign.dk/docs/v3/errors).
## Related
### Retrieve a Document
Source: https://docs.nextsign.dk/docs/v3/documents-get
Summary: Fetch one library document with GET /v3/api/documents/{id} — its versions and, for a .docx, the exact fields you can fill when creating a case.
Returns one document from your library: its versions, and — for a `.docx` — the fields a case built from it can fill, each with its type, label, whether it is required, its options and its bounds. This is the page to read before [creating a case from a document](https://docs.nextsign.dk/docs/v3/documents-create-case).
```text
GET https://api.nextsign.dk/v3/api/documents/{id}
```
Authenticated with an API key — see [Authorization](https://docs.nextsign.dk/docs/v3/authorization).
## Quickstart
```bash
curl https://api.nextsign.dk/v3/api/documents/6a58c2a06f829bebd9232c1b \
-H "Authorization: Bearer $NEXTSIGN_API_KEY"
```
```js
const response = await fetch(
`https://api.nextsign.dk/v3/api/documents/${documentId}`,
{ headers: { Authorization: `Bearer ${process.env.NEXTSIGN_API_KEY}` } },
);
const { document } = await response.json();
const required = document.version.fields.filter((f) => f.required).map((f) => f.path);
```
```python
import os, requests
response = requests.get(
f"https://api.nextsign.dk/v3/api/documents/{document_id}",
headers={"Authorization": f"Bearer {os.environ['NEXTSIGN_API_KEY']}"},
)
document = response.json()["document"]
required = [f["path"] for f in document["version"]["fields"] if f["required"]]
```
---
## Path Parameter
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | `string` | The document id, from [List Documents](https://docs.nextsign.dk/docs/v3/documents-list) |
## Query Parameters
| Parameter | Type | Default | Description |
| --------- | ---- | ------- | ----------- |
| `versionId` | `string` | current version | Describe a specific version instead of the current one. Must be a version of this document |
Every version is listed under `versions`; only the selected one has its fields expanded under `version`.
---
## Response
```json title="200 OK"
{
"document": {
"id": "6a394ed08ee8c3669f2c3e17",
"name": "Offer letter.docx",
"format": "docx",
"private": false,
"shared": false,
"createdAt": "2026-04-22T08:30:12.004Z",
"updatedAt": "2026-04-22T08:30:12.551Z",
"currentVersion": {
"id": "6a394ed08ee8c3669f2c3e1a",
"versionNumber": 1,
"name": "Offer letter.docx",
"format": "docx",
"createdAt": "2026-04-22T08:30:12.480Z",
"fieldCount": 5
},
"versions": [
{
"id": "6a394ed08ee8c3669f2c3e1a",
"versionNumber": 1,
"name": "Offer letter.docx",
"format": "docx",
"createdAt": "2026-04-22T08:30:12.480Z"
}
],
"version": {
"id": "6a394ed08ee8c3669f2c3e1a",
"versionNumber": 1,
"name": "Offer letter.docx",
"format": "docx",
"createdAt": "2026-04-22T08:30:12.480Z",
"fields": [
{
"path": "name",
"type": "string",
"subtype": "dropdown",
"label": "Name",
"description": null,
"required": true,
"default": null,
"options": [
{ "value": "inam", "label": "Inam" },
{ "value": "andreas", "label": "Andreas" }
],
"validations": null,
"visible": null,
"item": null,
"template": null
},
{
"path": "role",
"type": "string",
"subtype": null,
"label": "Role",
"description": "The position being offered.",
"required": false,
"default": null,
"options": null,
"validations": { "maxLength": 80 },
"visible": null,
"item": null,
"template": null
},
{
"path": "start_date",
"type": "string",
"subtype": "date",
"label": "Start date",
"description": null,
"required": false,
"default": null,
"options": null,
"validations": null,
"visible": null,
"item": null,
"template": null
},
{
"path": "is_senior",
"type": "boolean",
"subtype": "checkbox",
"label": "Is senior",
"description": null,
"required": false,
"default": null,
"options": null,
"validations": null,
"visible": null,
"item": null,
"template": null
},
{
"path": "terms",
"type": "array",
"subtype": null,
"label": "Terms",
"description": null,
"required": false,
"default": null,
"options": null,
"validations": null,
"visible": null,
"item": {
"type": "object",
"fields": [
{ "path": "title", "type": "string", "subtype": null, "label": "Title", "description": null, "required": false, "default": null, "options": null, "validations": null, "visible": null, "item": null, "template": null },
{ "path": "description", "type": "string", "subtype": "textarea", "label": "Description", "description": null, "required": false, "default": null, "options": null, "validations": null, "visible": null, "item": null, "template": null }
]
},
"template": null
}
],
"automaticFields": [
{ "path": "company_name", "source": "global" },
{ "path": "today", "source": "global" }
]
},
"access": null
}
}
```
The document-level fields are the ones [List Documents](https://docs.nextsign.dk/docs/v3/documents-list#response) returns, plus:
| Field | Description |
| ----- | ----------- |
| `versions` | Every version, newest first. Pin one with `versionId` when creating a case, otherwise the current version is used |
| `version` | The selected version, with its fields expanded |
| `version.fields` | What you can fill — see [Fields](#fields). Empty for a `pdf` |
| `version.automaticFields` | What exists in the document but is filled for you — see [Automatic fields](#automatic-fields). Sending a value for one of these is rejected |
| `access` | For a `shared` document, how it was shared: `{ "type": "referral" }` or `{ "type": "purchase" }`. `null` for your own |
---
## Fields
Each entry in `version.fields` is one value you may send under `tags` when creating a case, keyed by its `path`.
| Field | Description |
| ----- | ----------- |
| `path` | The key to send. Dotted for a field inside a group: `address.city` is sent as `{ "address": { "city": "…" } }` |
| `type` | `string`, `number`, `boolean` or `array` |
| `subtype` | A hint about the kind of string or number: `date`, `email`, `phone`, `url`, `textarea`, `dropdown`, `radio`, `address`, `template`, `checkbox`, … or `null` |
| `label` | What the dashboard shows for this field |
| `description` | Help text the document's author wrote, or `null` |
| `required` | Whether a value must be sent. A field with a `default` counts as filled |
| `default` | The value used when you send none |
| `options` | For a `dropdown` or `radio`: the allowed values. Send the `value`, not the `label` |
| `validations` | Bounds the author set — any of `minLength`, `maxLength`, `match` (a regular expression), `min`, `max` |
| `visible` | When set, the field only applies if the rule holds against the other values — see [Conditional fields](#conditional-fields) |
| `item` | For an `array`: the shape of one row |
| `template` | For a `template` string: the embedded `{tags}` you may fill — see [Template fields](#template-fields) |
### What to send, by type
| `type` | Send | Example |
| ------ | ---- | ------- |
| `string` | a string | `"Aarhus"` |
| `string` with `subtype: "date"` | any string `Date` can parse; ISO 8601 is safest | `"2026-09-01"` |
| `string` with `options` | one of the `options[].value` strings | `"andreas"` |
| `number` | a JSON number — not a numeric string | `37.5` |
| `boolean` | `true` or `false`. A required boolean must be `true` | `true` |
| `array` | a list of rows shaped like `item` — objects when `item.type` is `object`, scalars otherwise | `[{ "title": "Pay", "description": "Monthly" }]` |
| a group (dotted `path`) | nested objects | `{ "address": { "city": "Aarhus", "zip": "8000" } }` |
Values are inserted into the document as you send them — a number is not reformatted and a date is not localised. The dashboard formats what a person types; the API trusts that you already have.
### Conditional fields
A field with a `visible` rule set applies only when the rule holds:
```json
{
"path": "cvr",
"required": true,
"visible": { "match": "all", "rules": [{ "field": "is_company", "operator": "isTrue" }] }
}
```
Here `cvr` is required when `is_company` is `true`, and must **not** be sent otherwise — a value for a field that is hidden by its rule is rejected, because the produced document would silently ignore it. `match` is `all` or `any`; `operator` is one of `equals`, `notEquals`, `contains`, `gt`, `lt`, `isFilled`, `isEmpty`, `isTrue`, `isFalse`; `field` always names a top-level path.
### Template fields
A `subtype: "template"` string is a passage of editable text with `{tags}` inside it that the author chose to expose:
```json
{
"path": "intro",
"subtype": "template",
"default": "Dear {name}, welcome to {company_name}.",
"template": { "tags": [{ "key": "name", "label": "Name", "default": null }] }
}
```
Send either a plain string to replace the whole text, or `{ "text": "…", "values": { "name": "Bo" } }` to keep (or edit) the text and fill its tags. Tags that are not listed under `template.tags` resolve from the document's other values automatically.
---
## Automatic fields
`automaticFields` lists the keys that are in the document but are **not yours to fill**. Sending a value for any of them is a `400`:
| `source` | Filled from |
| -------- | ----------- |
| `global` | Your account — `{company_name}`, `{today}`, the sender's name and email, and the custom global fields set up under **Dashboard → Config**. See [Global fields](https://docs.nextsign.dk/docs/template-tags#global-fields) |
| `hidden` | The field's own `default`. The author marked it hidden so it renders as a constant |
| `computed` | Other fields — a sum, a lookup, a formatted copy — resolved when the document is produced |
---
## Errors
| `error` | Status | Meaning |
| ------- | ------ | ------- |
| `validation-failed` | `400` | The id is malformed, or `versionId` is not a version of this document. See `details` |
| `document-not-found` | `404` | No such document in your library |
```json title="400 Bad Request"
{
"error": "validation-failed",
"details": [{ "field": "versionId", "message": "is not a version of this document" }]
}
```
**`404` does not mean the document does not exist** — only that it is not yours to use. A document from another company, a colleague's private document, one that lives inside a public form, and an id that was never issued all answer identically, on purpose. See [What you see](https://docs.nextsign.dk/docs/v3/documents-list#what-you-see).
Reading never changes anything, so every request here is safe to retry. Full detail in [Errors](https://docs.nextsign.dk/docs/v3/errors).
## Related
### Create a Case from a Document
Source: https://docs.nextsign.dk/docs/v3/documents-create-case
Summary: Fill a library document and send it for signing with POST /v3/api/documents/{id}/cases — the document-first way to create a case.
Creates a case whose document is one from your library (**Dashboard → Documents**), fills its fields with the values you send, and — unless you ask for a draft — sends it.
```text
POST https://api.nextsign.dk/v3/api/documents/{id}/cases
```
Authenticated with an API key — see [Authorization](https://docs.nextsign.dk/docs/v3/authorization).
This is [Create a Case](https://docs.nextsign.dk/docs/v3/case-create) with the document fixed by the URL. It takes the same recipients, settings and sender, runs through the same pipeline, and returns the same case object — the only thing that changes is that the body describes **one document's values** instead of a `documents` array. Use it when your integration starts from a template; use `POST /v3/api/cases` when it starts from a file, or needs several documents.
## Quickstart
Find the document with [List Documents](https://docs.nextsign.dk/docs/v3/documents-list), read its fields with [Retrieve a Document](https://docs.nextsign.dk/docs/v3/documents-get), then:
```bash
curl https://api.nextsign.dk/v3/api/documents/6a394ed08ee8c3669f2c3e17/cases \
-H "Authorization: Bearer $NEXTSIGN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Offer letter — Andreas Lauridsen",
"recipients": [{ "name": "Andreas Lauridsen", "email": "al@example.com" }],
"tags": {
"name": "andreas",
"role": "Engineer",
"start_date": "2026-09-01",
"is_senior": true,
"terms": [{ "title": "Pay", "description": "Monthly, in arrears" }]
}
}'
```
```js
const response = await fetch(
`https://api.nextsign.dk/v3/api/documents/${documentId}/cases`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NEXTSIGN_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Offer letter — Andreas Lauridsen',
recipients: [{ name: 'Andreas Lauridsen', email: 'al@example.com' }],
tags: {
name: 'andreas',
role: 'Engineer',
start_date: '2026-09-01',
is_senior: true,
terms: [{ title: 'Pay', description: 'Monthly, in arrears' }],
},
}),
},
);
const { case: created } = await response.json();
console.log(created.recipients[0].signingUrl);
```
```python
import os, requests
response = requests.post(
f"https://api.nextsign.dk/v3/api/documents/{document_id}/cases",
headers={"Authorization": f"Bearer {os.environ['NEXTSIGN_API_KEY']}"},
json={
"title": "Offer letter — Andreas Lauridsen",
"recipients": [{"name": "Andreas Lauridsen", "email": "al@example.com"}],
"tags": {
"name": "andreas",
"role": "Engineer",
"start_date": "2026-09-01",
"is_senior": True,
"terms": [{"title": "Pay", "description": "Monthly, in arrears"}],
},
},
)
created = response.json()["case"]
print(created["recipients"][0]["signingUrl"])
```
You get back `201` with the whole case — the same object, field for field, that [Create a Case](https://docs.nextsign.dk/docs/v3/case-create#response-fields) returns, and a `Location` header pointing at `/v3/api/cases/{id}`:
```json title="201 Created"
{
"case": {
"id": "6a87fdd9c2dce19673cd81d2",
"referenceId": null,
"title": "Offer letter — Andreas Lauridsen",
"state": "open",
"language": "da",
"createdAt": "2026-08-21T10:12:41.803Z",
"expiresAt": "2026-09-20T10:12:41.803Z",
"folder": { "id": "65ab12cd34ef56ab78cd90a2", "name": "Default" },
"sender": { "id": "65ab12cd34ef56ab78cd90ef", "name": "Anna Beck", "email": "anna@example.com" },
"activeGroup": 0,
"recipients": [
{
"id": "6a87fdd9c2dce19673cd81d3",
"uid": "4t5ZQbtB",
"name": "Andreas Lauridsen",
"email": "al@example.com",
"phone": "",
"position": "",
"group": 0,
"type": "email",
"signing": true,
"status": "pending",
"needsCpr": false,
"signingSchema": "",
"emailStatus": null,
"signingUrl": "https://www.nextsign.dk/sign/6a87fdd9c2dce19673cd81d2/2/h6gKDYSiPyYTC5Bkq6urKfKKW"
}
],
"documents": [
{
"id": "6a87fdd9c2dce19673cd81d4",
"name": "Offer letter.pdf",
"type": "application/pdf",
"url": "https://nextsign-de.fsn1.your-objectstorage.com/65ab/production/Offer letter.pdf",
"signObligated": true,
"documentMustBeRead": false,
"signatories": []
}
],
"delivery": {
"attempted": true,
"email": { "ok": true },
"eboks": { "ok": true },
"sms": { "ok": true },
"webhook": { "ok": true }
},
"notices": [
{
"code": "sender-defaulted-to-key-creator",
"message": "Neither senderId nor senderEmail was provided. The case was attributed to the API key's creator (anna@example.com)."
},
{
"code": "default-signing-schemas",
"message": "No settings.signingSchemas provided — the standard eIDs were used.",
"using": [
"urn:grn:authn:dk:mitid:low",
"urn:grn:authn:dk:mitid:substantial",
"urn:grn:authn:dk:mitid:business",
"urn:grn:authn:se:bankid",
"urn:grn:authn:de:personalausweis"
]
}
]
}
}
```
The `.docx` was filled with your values and rendered to PDF on the way in, so the attached document is `Offer letter.pdf` with `type: application/pdf` — both describe the file at `url`, not the library source.
**Unknown fields are rejected** — in the body and inside `tags`. A misspelled tag key is a `400`, not a blank in the produced document. See [Tags](#tags).
## Path Parameter
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | `string` | The document id, from [List Documents](https://docs.nextsign.dk/docs/v3/documents-list) |
## Query Parameters
| Parameter | Type | Default | Description |
| --------- | ---- | ------- | ----------- |
| `publicUrls` | `boolean` | `false` | Return downloadable document links. See [Document URLs](https://docs.nextsign.dk/docs/v3/case-create#document-urls) |
## Request Body
Everything from [Create a Case](https://docs.nextsign.dk/docs/v3/case-create#request-body) except `documents` and the case-level `tags` array, plus the document's own fields:
| Field | Type | Required | Default | Description |
| ----- | ---- | -------- | ------- | ----------- |
| `title` | `string` | **Yes** | — | Case title. Max 300 characters |
| `recipients` | `array` | Conditional | — | Max 100. **Required when `state` is `open`**. See [Recipients](https://docs.nextsign.dk/docs/v3/case-create#recipients) |
| `tags` | `object` | No | — | Values for the document's fields, keyed by `path`. See [Tags](#tags) |
| `versionId` | `string` | No | current version | Pin a specific version of the document. Must be one of its `versions` |
| `signObligated` | `boolean` | No | `true` | Whether signing the document is required. `false` attaches it for reading only |
| `documentMustBeRead` | `boolean` | No | `false` | Require each recipient to open it before signing |
| `signatories` | `array` | No | all | Indexes into `recipients` — who signs the document. `[0, 2]` is the first and third recipient |
| `state` | `string` | No | `open` | `open` sends the case, `draft` saves it for later editing |
| `referenceId` | `string` | No | — | Your own reference. Max 200 characters |
| `folder` | `string` | No | company default | Folder **name or id**. See [Folders](https://docs.nextsign.dk/docs/v3/case-create#folders) |
| `settings` | `object` | No | — | Language, expiry, reminders, signing methods, … See [Settings](https://docs.nextsign.dk/docs/v3/case-create#settings) |
| `messageTemplateId` | `string` | No | — | Template for the message recipients receive |
| `presetId` | `string` | No | — | Preset id. See [Presets](https://docs.nextsign.dk/docs/v3/case-create#presets) |
| `senderId` | `string` | No | the key's creator | Attribute the case to a colleague, by user id. See [Sender](https://docs.nextsign.dk/docs/v3/case-create#sender) |
| `senderEmail` | `string` | No | the key's creator | Attribute the case to a colleague, by email |
There is no `name`: the attached file keeps the library version's own name, with `.docx` becoming `.pdf` once rendered.
---
## Tags
`tags` is an object keyed by the `path` of each field in [`version.fields`](https://docs.nextsign.dk/docs/v3/documents-get#fields). Send nested fields as nested objects, lists as arrays of rows, and a `template` field as a string or `{ "text", "values" }`:
```json
{
"tags": {
"employee_name": "Andreas Lauridsen",
"start_date": "2026-09-01",
"weekly_hours": 37,
"address": { "city": "Aarhus", "zip": "8000" },
"other_benefits": [
{ "name": "Phone", "value": "Paid by employer" },
{ "name": "Pension", "value": "8% employer contribution" }
],
"intro": { "text": "Dear {name}, welcome aboard.", "values": { "name": "Andreas" } }
}
}
```
The values are checked against the document's field definitions before anything is created — the same checks the dashboard's "Document fields" step runs — and every problem comes back at once:
- **Unknown keys** are rejected. So are values for an [automatic field](https://docs.nextsign.dk/docs/v3/documents-get#automatic-fields) — a global, hidden or computed one — because the produced document would ignore or overwrite them without telling you.
- **Required fields must be filled**, unless they carry a `default`. A required `boolean` must be `true`.
- **Types are enforced.** A `number` field takes a JSON number, a `boolean` a boolean, an `array` a list of rows; a `dropdown` or `radio` takes one of its `options[].value`; a `date` must parse as a date; the author's `minLength`, `maxLength`, `match`, `min` and `max` bounds apply.
- **Conditional fields follow their rule.** A field whose `visible` rule does not hold against the values you sent is neither required nor accepted. See [Conditional fields](https://docs.nextsign.dk/docs/v3/documents-get#conditional-fields).
```json title="400 Bad Request"
{
"error": "validation-failed",
"details": [
{ "field": "tags.nope", "message": "unknown field" },
{ "field": "tags.company_name", "message": "is a global field and is filled from the account" },
{ "field": "tags.name", "message": "must be one of: inam, andreas" },
{ "field": "tags.is_senior", "message": "must be a boolean" },
{ "field": "tags.terms[0].bogus", "message": "unknown field" },
{ "field": "tags.employee_name", "message": "is required" }
]
}
```
Values are inserted as you send them. Automatic fields — the company name, today's date, the sender's details — are added at production time, and hidden and computed fields resolve from their definitions; you never need to send those.
**A `pdf` document has no fields.** Sending `tags` with any key for one returns `400` at `tags` (an empty object is accepted). Check `format` on the document first.
### Drafts keep the values editable
With `state: "draft"` the document is attached **by reference** and not rendered: the draft opens in the dashboard's case builder with your `tags` pre-filled and still editable, and the PDF is produced when someone sends it from there. In the response the document has the library file's name and type, and `url` is `null` until then.
---
## Errors
| `error` | Status | Meaning |
| ------- | ------ | ------- |
| `validation-failed` | `400` | A body field or a tag value is invalid, or `versionId` is not a version of this document. See `details` |
| `document-not-found` | `404` | No such document in your library. See [What you see](https://docs.nextsign.dk/docs/v3/documents-list#what-you-see) |
| `document-production-failed` | `400` | The document could not be rendered with these values — usually a `.docx` that Word itself would refuse to open |
| `invalid-folder` | `400` | No folder with that name or id |
| `message-template-not-found` | `400` | The case's `messageTemplateId`, or a recipient's, was not found. `details` names each one, e.g. `recipients[1].messageTemplateId` |
| `preset-not-found` | `400` | Not found for your company |
| `sender-user-not-found` | `400` | `senderId` or `senderEmail` is not a member of your company |
| `invalid-signing-page-logo` | `400` | `settings.logo` is not a PNG/JPEG, and not a NextSign URL |
Field paths are relative to the body — `tags.start_date`, `signatories[0]`, `versionId` — rather than the `documents[0].…` form the generic endpoint uses.
**Every `4xx` from this endpoint means nothing was created.** The document is rendered and the case persisted only after all of these have passed, so a `4xx` is always safe to fix and resend. Full detail in [Errors](https://docs.nextsign.dk/docs/v3/errors).
## Related
## Platform
Error handling and retry semantics for the v3 surface.
### Errors
Source: https://docs.nextsign.dk/docs/v3/errors
Summary: The v3 error envelope, the full list of error slugs, and what is safe to retry.
v3 uses the error envelope below. The case-creation retry guarantees on this page apply to case endpoints. **Webflows use a required idempotency key:** see [webflow retries and errors](https://docs.nextsign.dk/docs/v3/webflows-create#retries-and-invitation-status). A webflow `409` means an earlier operation already used the key; a server error can occur after the webflow was saved.
## The envelope
Every error carries a stable machine-readable `error` slug:
```json
{ "error": "invalid-folder" }
```
Anything caused by the request body also carries `details`, with one entry per problem:
```json title="400 Bad Request"
{
"error": "validation-failed",
"details": [
{ "field": "title", "message": "is required" },
{ "field": "recipients[1].phone", "message": "is required" },
{ "field": "settings.reminders.amount", "message": "must be at least 1" }
]
}
```
`field` is a path into your payload, including array indexes, so you can point straight at the offending value.
Branch on `error`, not on `message`. Slugs are stable; the human-readable text may be reworded.
## Validation returns everything at once
Structural problems — wrong types, bad formats, unknown fields, out-of-range values — are collected and returned together, so you fix them in one round-trip instead of one per deploy.
Three checks read the structure and so run once it is sound: whether an `open` case actually has recipients and documents, whether a document's `content` decodes to a PDF or `.docx`, and whether a library document's `tags` match the fields that document defines. If the body also has a structural problem you will see these on the next request, after the structural fixes — never a case created with one still outstanding.
Tag values are checked field by field, so the `field` path reaches into them — `documents[0].tags.address.city`, or `tags.terms[1].qty` on the [document-first endpoint](https://docs.nextsign.dk/docs/v3/documents-create-case):
```json
{ "field": "documents[0].tags.start_date", "message": "must be a valid date" }
```
`unknown field` appears for any key not in the documented schema — including a misspelled one:
```json
{ "field": "recipeints", "message": "unknown field" }
```
Query parameters are held to the same standard. On the endpoints that take them, `field` is the parameter name rather than a body path, and an unrecognised parameter is rejected exactly as an unknown body field is — a filter that was quietly ignored is the one mistake you could not spot in the response.
```json
{ "field": "createdAfter", "message": "must be an ISO 8601 date, e.g. 2026-01-31 or 2026-01-31T09:00:00Z" }
```
## Status codes
| Status | Meaning | Safe to retry? |
| ------ | ------- | -------------- |
| `200` | Success. Nothing was changed — this was a read | Yes, always |
| `201` | Created. The case exists | **No** — retrying creates a second case |
| `400` | Your request was rejected. **Nothing was created** | Yes, after fixing it |
| `401` | Authentication failed | Only after fixing the key |
| `403` | Authenticated, but not allowed | No |
| `404` | No such case or document, or not yours to read | No — it will not appear later |
| `500` | Something failed on our side | Yes, with backoff |
## The one guarantee worth designing around
**A `4xx` from a case-creation endpoint means nothing was created by that request.**
The case is written to the database only after authentication, validation, folder and template resolution, and document upload and conversion have all succeeded. Everything that can reject your request happens before that point.
Once the case exists, v3 will not turn around and report an error for it. Sending is isolated per channel and reported in `delivery`:
```json title="201 Created"
{
"case": {
"id": "65ab12cd34ef56ab78cd90b0",
"delivery": {
"attempted": true,
"email": { "ok": false, "error": "ECONNREFUSED" },
"eboks": { "ok": true },
"sms": { "ok": true },
"webhook": { "ok": true }
}
}
}
```
This is the one guarantee to build your retry logic on. There is no window in which a case exists and the response says it failed, so you never have to ask "did that actually go through?" before retrying — the status code answers it.
Handle a failed channel as an operational event — alert, resend from the dashboard — not as a failed creation.
## Authentication errors
| Status | `error` | Meaning |
| ------ | ------- | ------- |
| `401` | `missing-token` | No `Authorization` header |
| `401` | `invalid-token` | The key does not exist |
| `401` | `key-revoked` | The key was revoked |
| `401` | `key-expired` | The key passed its expiry date |
| `403` | `company-disabled` | The company account is disabled |
| `403` | `key-has-no-user` | The key has nobody to attribute cases to. Keys made in the dashboard always do — tell us if you see this |
See [Authorization](https://docs.nextsign.dk/docs/v3/authorization).
## Request errors
| `error` | Status | Meaning |
| ------- | ------ | ------- |
| `validation-failed` | `400` | One or more fields are invalid. See `details` |
| `form-not-found` | `404` | No published webflow-enabled form in your company |
| `webflow-not-found` | `404` | No accessible webflow with this ID |
| `sender-not-in-company` | `403` | The webflow sender is no longer a company member |
| `idempotency-conflict` | `409` | A webflow request used this key with a different payload |
| `case-not-found` | `404` | No case with that id in your company |
| `document-not-found` | `404` | On the `/v3/api/documents` endpoints: no such document in your library, or not visible to this key. See [What you see](https://docs.nextsign.dk/docs/v3/documents-list#what-you-see) |
| `invalid-folder` | `400` | No folder with that name or id |
| `message-template-not-found` | `400` | The case's `messageTemplateId`, or a recipient's, was not found. `details` names each one |
| `preset-not-found` | `400` | `presetId` is not in your company |
| `sender-user-not-found` | `400` | `senderId` or `senderEmail` is not a member of your company |
| `invalid-signing-page-logo` | `400` | `settings.logo` is not a PNG/JPEG, and not a NextSign URL |
| `document-not-found` | `400` | On `POST /v3/api/cases`: a `documentId` is not in your library, or not visible to this key. `details` names the document |
| `document-version-not-found` | `400` | `versionId` does not belong to that document |
| `document-conversion-failed` | `400` | The `.docx` could not be converted |
| `document-production-failed` | `400` | A library document could not be produced |
| `unsupported-file-type` | `400` | A safety net behind the `content` check. You should never see it — tell us if you do |
Document errors name the offending item:
```json
{
"error": "document-conversion-failed",
"details": [
{ "field": "documents[1]", "message": "the file could not be converted — is it a valid Word document?" }
]
}
```
A file can be a well-formed `.docx` container and still be unopenable — a ZIP missing its relationship parts is a valid archive and a broken document. That is what this error means.
## Notices are not errors
`notices` appears on **successful** responses and records what was defaulted, ignored or degraded:
| `code` | Meaning |
| ------ | ------- |
| `sender-defaulted-to-key-creator` | No `senderId` or `senderEmail` sent; the case was attributed to whoever created the API key |
| `default-signing-schemas` | No `settings.signingSchemas` sent; the standard eIDs were used |
| `no-sharepoint-integration` | `settings.integrations.microsoft.returnPath` was ignored |
| `contact-not-saved` | A recipient could not be added to your contact list |
| `signing-page-logo-not-stored` | The logo could not be stored; the case was created without it |
| `signing-urls-unavailable` | The case exists — it was created, or you are reading it back — but its signing links could not be built |
Log them. Do not treat them as failures.
A notice always has a `code` and a `message`, and may carry extra fields describing what was used. `default-signing-schemas` reports the set it fell back to:
```json
{
"code": "default-signing-schemas",
"message": "No settings.signingSchemas provided — the standard eIDs were used.",
"using": [
"urn:grn:authn:dk:mitid:low",
"urn:grn:authn:dk:mitid:substantial",
"urn:grn:authn:dk:mitid:business",
"urn:grn:authn:se:bankid",
"urn:grn:authn:de:personalausweis"
]
}
```
## Suggested client handling
```js
const response = await fetch('https://api.nextsign.dk/v3/api/cases', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NEXTSIGN_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const body = await response.json();
if (response.status === 201) {
// The case exists. Never retry from here.
for (const [channel, result] of Object.entries(body.case.delivery)) {
if (result?.ok === false) alertOps(channel, result.error, body.case.id);
}
return body.case;
}
if (response.status === 400) {
// Nothing was created. Fix and resend.
throw new BadPayload(body.error, body.details);
}
if (response.status >= 500) {
return retryWithBackoff(); // safe: no case was created
}
throw new AuthError(body.error);
```
```python
response = requests.post(
"https://api.nextsign.dk/v3/api/cases",
headers={"Authorization": f"Bearer {os.environ['NEXTSIGN_API_KEY']}"},
json=payload,
)
body = response.json()
if response.status_code == 201:
# The case exists. Never retry from here.
for channel, result in body["case"]["delivery"].items():
if isinstance(result, dict) and result.get("ok") is False:
alert_ops(channel, result.get("error"), body["case"]["id"])
return body["case"]
if response.status_code == 400:
# Nothing was created. Fix and resend.
raise BadPayload(body["error"], body.get("details"))
if response.status_code >= 500:
return retry_with_backoff() # safe: no case was created
raise AuthError(body["error"])
```
Case creation has no idempotency key yet, so a retried `201` creates a second case. Retry only on network failures and `5xx`.
## Related
## Getting Started
What the NextSign MCP server is, and how to connect it to your AI assistant.
### Overview
Source: https://docs.nextsign.dk/docs/mcp
Summary: Let your AI assistant create and send NextSign signing cases for you — connect once, then just ask in plain language.
NextSign has an MCP server — a small add-on that connects NextSign to AI assistants like Claude and ChatGPT. Once connected, you can ask your assistant in plain language to send a document for signature, and it fills in the details and creates the case for you. Everything it creates shows up in your NextSign dashboard, exactly as if you had made it there.
MCP (Model Context Protocol) is the standard way AI assistants connect to outside tools. You don't need to know anything about it beyond one address and a login.
**Beta.** The server exposes four tools today — creating and sending signing cases, and browsing your document library to create cases from your own templates. More will follow, and details may change before general availability. The gaps are listed under [Troubleshooting](https://docs.nextsign.dk/docs/mcp/troubleshooting#beta-limitations).
## What you can ask for
- *"Send contract.pdf to jane@example.com for signature."*
- *"Create a signing case for the Q3 agreement — Anna signs first, then Jonas."*
- *"Send this to +45 12 34 56 78 by SMS for signing."*
- *"Set it up as a draft so I can review it in NextSign before it goes out."*
- *"Send our Employment Contract template to Andreas — he starts 1 September at 37 hours a week."*
- *"Which templates do we have in NextSign, and what does the offer letter need filled in?"*
## The server address
```text
https://api.nextsign.dk/v3/mcp
```
You paste this once, during setup — the [Connect](https://docs.nextsign.dk/docs/mcp/connect) page walks through it. For the technically curious: it is a Streamable HTTP MCP server (stateless, no SSE), with OAuth sign-in or [API-key](https://docs.nextsign.dk/docs/v3/authorization) authentication.
## What you need
- A NextSign account that is a member of a company.
- An AI assistant that supports MCP connectors — Claude and ChatGPT both do.
- Nothing else. Sign-in happens with your normal NextSign login during setup.
## Where to start
### Connect Your Assistant
Source: https://docs.nextsign.dk/docs/mcp/connect
Summary: Add the NextSign MCP server to Claude or ChatGPT, sign in, and check that it works.
Connecting takes a few minutes and no code. You add one address to your assistant, sign in with your normal NextSign login, and approve the connection.
```text
https://api.nextsign.dk/v3/mcp
```
### Add the server in your assistant
Works on claude.ai and in the Claude desktop app.
1. Open **Settings → Connectors**.
2. Click **Add custom connector**.
3. Name it **NextSign** and paste the server address above.
4. Click **Add** — no key or password goes in here. Claude discovers the NextSign sign-in on its own.
Custom connectors require a paid plan, and on some plans **Developer mode** must be switched on first under **Settings → Apps & Connectors → Advanced settings**.
1. Open **Settings → Apps & Connectors**.
2. Click **Create** (or **Add custom connector**).
3. Name it **NextSign**, paste the server address above, and pick **OAuth** as authentication.
4. Save. ChatGPT sends you to the NextSign sign-in described in the next step.
Only needed if you prefer an [API key](https://docs.nextsign.dk/docs/v3/authorization) over signing in — for example a shared machine that should always act as one specific key. Requires [Node.js](https://nodejs.org) installed.
Open **Settings → Developer → Edit Config** and add:
```json title="claude_desktop_config.json"
{
"mcpServers": {
"nextsign": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.nextsign.dk/v3/mcp",
"--header",
"Authorization:${NEXTSIGN_AUTH}"
],
"env": {
"NEXTSIGN_AUTH": "Bearer key_YOUR_KEY_HERE"
}
}
}
}
```
Replace `key_YOUR_KEY_HERE` with your key, then restart the app. (The env-var indirection is deliberate — a space inside `--header` arguments gets split by the bridge.)
Any MCP client that supports **remote servers over Streamable HTTP** works. Two ways in:
- **OAuth** (preferred): point the client at the server address and let it run the standard MCP authorization flow. Registration is dynamic — no pre-shared client id needed.
- **API key**: if the client can send custom headers, set `Authorization: Bearer key_…` with a key from [Authorization](https://docs.nextsign.dk/docs/v3/authorization).
Clients that only speak the older SSE transport cannot connect.
### Sign in and approve
With OAuth, a NextSign window opens:
1. Log in if you aren't already.
2. If you belong to more than one company, pick which company the assistant may act for.
3. Click **Approve**.
Cases the assistant creates are attributed to **you**, in the company you picked — they appear in the dashboard under your name, and signing emails show you as the sender. The connection acts for that one company only; connect again to switch.
### Check it works
Ask your assistant:
> What NextSign tools do you have?
It should mention **create_case** ("Create a signing case") along with **list_documents**, **get_document** and **create_case_from_document**. Then head to [Using It](https://docs.nextsign.dk/docs/mcp/usage) for what to ask next.
## Managing connections
Behind the scenes, approving a connection issues a normal [API key](https://docs.nextsign.dk/docs/v3/authorization) for the assistant. It appears in the dashboard under **Configurations → API keys**, named after the assistant (e.g. *"ChatGPT (MCP)"*). Deleting that key disconnects the assistant immediately — it will ask to connect again the next time someone uses it.
## Related
### Using It
Source: https://docs.nextsign.dk/docs/mcp/usage
Summary: Example prompts, what happens when the assistant creates a case, and habits that keep you safe.
Once connected, you talk to your assistant normally. When a request involves sending something for signature, the assistant calls NextSign's `create_case` tool with the details you gave it — or, when you name one of your templates, looks it up in your document library and calls `create_case_from_document` with the fields filled in.
## Example prompts
| You say | What the assistant sets up |
| ------- | -------------------------- |
| "Send contract.pdf to jane@example.com for signature." | One case, one signer, sent immediately by email |
| "Anna (anna@firm.dk) signs first, then Jonas (jonas@firm.dk)." | Two signers in order — Jonas is only asked once Anna has signed |
| "Send it to +45 12 34 56 78 by SMS." | An SMS signer — a phone number replaces the email |
| "Make it a draft so I can check it first." | A draft case — nothing is sent until you open it in the dashboard and send it yourself |
| "It should expire in 14 days and remind them every 3 days." | Expiry and automatic reminders on the case |
| "Which templates do we have?" | Lists your document library — names, whether each is a fillable template, and how many fields it has |
| "Send our Employment Contract to Andreas, starting 1 September, 37 hours a week." | Finds the template, reads its fields, fills what you said, asks about any required field you did not mention, then creates and sends the case |
## What actually happens
1. The assistant calls `create_case` with a title, the signers, and the documents.
2. NextSign creates the case and — unless it's a draft — emails or texts every signer their signing link.
3. The case appears in your dashboard immediately, attributed to you, and the assistant reports back the case id, its state, and the delivery status per signer.
The assistant only knows what you tell it. Exact names, correctly spelled email addresses, and which document to use are your job — it cannot look colleagues up in your address book.
## How the assistant gets the document
Three ways, depending on what you have:
- **A link** — a public `https://` URL the document can be fetched from.
- **The file itself** — attach it in the conversation and the assistant sends the contents along (up to 20 MB). Whether attachments reach the tool depends on the assistant app.
- **Your NextSign library** — a document that already lives in NextSign (**Dashboard → Documents**). The assistant can list the library, read which fields a template needs, and fill them from what you tell it. Fields it cannot fill from your message, it will ask about — a required field left blank is rejected rather than sent empty.
## Working from a template
A `.docx` in your library is a template with `{tag}` fields — a name, a start date, a list of terms. When you ask for a case from one, the assistant:
1. Finds the document with `list_documents` (by the name you used).
2. Reads its fields with `get_document` — which are required, which are dropdowns, which depend on others.
3. Builds the values from your message and asks you for anything required that is missing.
4. Calls `create_case_from_document`, which fills the template, renders it to PDF and sends it.
The library the assistant sees is the one you would see in the dashboard: the company's shared documents, your own private ones, and anything another company has shared with yours. A colleague's private template is not available to it.
## Good habits
**Ask for a draft when unsure.** A draft case sends nothing — you review it in the dashboard and press send yourself. There is no undo for a sent case; recalling it means deleting the case after signers were already emailed.
**Give complete details up front.** One message with title, signers (in order, with addresses), and the document beats a back-and-forth where something gets lost.
**Asking twice creates two cases.** If the assistant reports an error or a timeout, check the dashboard before asking again — the case may already exist, and a second request would email your signers a second case. See [Troubleshooting](https://docs.nextsign.dk/docs/mcp/troubleshooting#duplicate-cases).
## Related
## Tools
The tools the server exposes, and every field they accept.
### create_case
Source: https://docs.nextsign.dk/docs/mcp/create-case
Summary: Every field the create_case tool accepts, and what it returns — the same payload as POST /v3/api/cases.
The general-purpose tool for creating cases. It accepts **the same payload as [`POST /v3/api/cases`](https://docs.nextsign.dk/docs/v3/case-create)** and runs through the same handler — an MCP call and a REST call with the same body behave identically. This page lists the fields so the MCP section stands alone; the REST page carries the deeper narrative and response examples.
The schema is **strict**: unknown fields are rejected, not ignored, and every problem in a payload comes back at once, each with a `field` path.
The company is fixed by the connection (the OAuth grant or the API key) — it can never be chosen in the payload. When neither `senderId` nor `senderEmail` is given, the case is attributed to the connected user, and the result says so with a `sender-defaulted-to-key-creator` notice. The REST endpoint's `publicUrls` query flag is not available over MCP.
## Top-level fields
| Field | Type | Notes |
| ----- | ---- | ----- |
| `title` | string, 1–300 | **Required** |
| `referenceId` | string ≤ 200 | Your own external reference |
| `state` | `open` \| `draft` | Default `open`. `open` requires recipients and documents and sends the case; `draft` creates an incomplete case for later |
| `folder` | string ≤ 200 | Folder name in the dashboard |
| `senderId` | ObjectId | Attribute the case to this company member (wins over `senderEmail`) |
| `senderEmail` | email | Attribute by address instead |
| `messageTemplateId` | ObjectId | Message template for the invitation |
| `presetId` | ObjectId | Preset that fills unset settings (and overrides locked fields) |
| `tags` | array ≤ 500 | Template-tag values for the documents |
| `settings` | object | See [Settings](#settings) — defaults apply even when omitted |
| `recipients` | array ≤ 100 | **Required non-empty when `state` is `open`** |
| `documents` | array ≤ 20 | **Required non-empty when `state` is `open`** |
## Recipient
| Field | Type | Notes |
| ----- | ---- | ----- |
| `name` | string, 1–200 | **Required** |
| `email` | email | **Required when `type` is `email`** (the default). Not needed for `sms` or `eboks` recipients |
| `phone` | string ≤ 40 | **Required when `type` is `sms`** |
| `signing` | boolean | Default `true`; `false` makes the recipient a viewer |
| `group` | int 0–100 | Signing order — group 0 signs first, group 1 is asked when group 0 is done. Default 0 |
| `type` | `email` \| `sms` \| `eboks` | How the signing link is delivered. Default `email` |
| `position` | string ≤ 200 | Shown under the signature |
| `needsCpr` | boolean | Require CPR validation. Default `false` |
| `cpr` | string ≤ 20 | Pre-filled CPR for validation |
| `signingSchema` | string | A specific eID (acr value) this recipient must use |
| `redirectUrl` | https URL | Where the signer lands after signing |
| `eboks` | object | e-Boks delivery details (see [e-Boks object](https://docs.nextsign.dk/docs/objects#e-boks-object)) |
| `message` | string ≤ 2000 | Personal message to this recipient |
| `messageTemplateId` | ObjectId | Per-recipient message template |
## Document
| Field | Type | Notes |
| ----- | ---- | ----- |
| `name` | string, 1–300 | **Required** |
| `url` | http(s) URL | Fetch the file from here |
| `content` | base64 string | The file itself, ≤ 20 MB decoded (data-URL prefixes are stripped) |
| `documentId` | ObjectId | A document from your NextSign library — see [`list_documents`](https://docs.nextsign.dk/docs/mcp/list-documents) |
| `versionId` | ObjectId | A specific library version — requires `documentId` |
| `tags` | object | Values for a library `.docx`'s fields, keyed by path. Checked against the document exactly as in [`create_case_from_document`](https://docs.nextsign.dk/docs/mcp/create-case-from-document#tags) |
| `signObligated` | boolean | Must be signed. Default `true` |
| `documentMustBeRead` | boolean | Must be opened before signing. Default `false` |
| `signatories` | int array | Indexes into `recipients` — which of them sign this document. Default: all signers |
**Exactly one of `url`, `content`, or `documentId`** must be set per document. For a case built around a single library template, [`create_case_from_document`](https://docs.nextsign.dk/docs/mcp/create-case-from-document) is the simpler call.
## Settings
| Field | Type | Notes |
| ----- | ---- | ----- |
| `autoSend` | boolean | Send to recipients on creation. Default `true` |
| `language` | string | Case language (`da`, `en`, …) |
| `template` | int 1–99 | Email template number |
| `expiresInDays` | int 1–3650 | Signing deadline |
| `message` | string ≤ 5000 | Message shown to all recipients |
| `attachSignedFiles` | boolean | Attach signed PDFs to the completion mail |
| `signingSchemas` | string array ≤ 50 | Allowed eIDs for the whole case (see [Signing Schemas](https://docs.nextsign.dk/docs/objects#signing-schemas)) |
| `logo` | string ≤ 8 MB | Base64 image or URL for the case logo |
| `reminders` | object | `{ send, amount 1–20, daysBetween 1–365, autoSend }` |
| `integrations.microsoft.returnPath` | string | Site-relative SharePoint path for signed files, e.g. `/sites/MySite/Shared Documents` |
## Example call
```json
{
"title": "Consulting agreement — Q3",
"recipients": [
{ "name": "Jane Jensen", "email": "jane@example.com" }
],
"documents": [
{ "name": "Agreement.pdf", "url": "https://example.com/agreement.pdf" }
],
"settings": { "expiresInDays": 14 }
}
```
## Result
The tool returns JSON. On success, the same `case` object as the REST endpoint — id, state, recipients with delivery status, documents, and any notices:
```json
{
"case": {
"id": "665f1c2ab4d746b7bb394001",
"title": "Consulting agreement — Q3",
"state": "open",
"recipients": [ { "name": "Jane Jensen", "delivery": { "email": "sent" } } ]
}
}
```
On failure the result is marked as an error and explains itself:
```json
{
"error": "validation-failed",
"details": [
{ "field": "recipients.0.email", "message": "recipient email is required" }
]
}
```
A malformed payload that doesn't match the schema at all is rejected by the protocol layer as an *invalid params* error before the tool runs — same information, different envelope.
## Related
### list_documents
Source: https://docs.nextsign.dk/docs/mcp/list-documents
Summary: The list_documents tool — the documents in your library that the assistant can turn into a signing case.
Lists the documents in your NextSign library (**Dashboard → Documents**) that the connection may use as the source of a case, newest first. It is the same operation as [`GET /v3/api/documents`](https://docs.nextsign.dk/docs/v3/documents-list) and runs through the same handler.
The assistant typically calls this on its own when you mention a template by name — *"send our Employment Contract to …"* — to find the right document id before calling [`get_document`](https://docs.nextsign.dk/docs/mcp/get-document) and [`create_case_from_document`](https://docs.nextsign.dk/docs/mcp/create-case-from-document).
## Arguments
| Field | Type | Notes |
| ----- | ---- | ----- |
| `limit` | int 1–100 | Documents per page. Default 25 |
| `cursor` | string | `nextCursor` from a previous result, to fetch the next page |
Unknown arguments are rejected.
## Result
```json
{
"documents": [
{
"id": "6a58c2a06f829bebd9232c1b",
"name": "Employment Contract.docx",
"format": "docx",
"private": true,
"shared": false,
"createdAt": "2026-05-12T09:14:08.201Z",
"updatedAt": "2026-05-12T09:14:09.377Z",
"currentVersion": {
"id": "6a58c2a16f829bebd9232c1e",
"versionNumber": 1,
"name": "Employment Contract.docx",
"format": "docx",
"createdAt": "2026-05-12T09:14:09.301Z",
"fieldCount": 27
}
}
],
"nextCursor": null,
"hasMore": false
}
```
`format` tells the assistant what it is dealing with: a `docx` is a template with `fieldCount` fields to fill, a `pdf` is attached as it is. The field definitions themselves come from [`get_document`](https://docs.nextsign.dk/docs/mcp/get-document).
**What is listed** is what the connected user would see in the dashboard: the company's non-private documents, the ones that user created, and any another company has shared with yours. A colleague's private document is not listed and cannot be used. See [What you see](https://docs.nextsign.dk/docs/v3/documents-list#what-you-see).
## Related
### get_document
Source: https://docs.nextsign.dk/docs/mcp/get-document
Summary: The get_document tool — one library document, its versions, and the exact fields to fill when creating a case from it.
Fetches one document from your library: its versions and, for a `.docx`, the fields a case built from it can fill — each with its path, type, label, whether it is required, its options and its bounds. It is the same operation as [`GET /v3/api/documents/{id}`](https://docs.nextsign.dk/docs/v3/documents-get) and runs through the same handler.
This is how the assistant learns what to ask you for. Given a 27-field employment contract it will read the fields, fill what it can from your message, and ask about the required ones that are still missing — rather than guessing.
## Arguments
| Field | Type | Notes |
| ----- | ---- | ----- |
| `id` | ObjectId | **Required.** The document id, from [`list_documents`](https://docs.nextsign.dk/docs/mcp/list-documents) |
| `versionId` | ObjectId | Describe a specific version instead of the current one |
## Result
The document, its `versions`, and the selected `version` with two lists:
- **`fields`** — what may be sent as `tags`. Each has a `path` (the key to send; dotted for fields inside a group), a `type` (`string`, `number`, `boolean`, `array`), a `subtype` hint (`date`, `dropdown`, `template`, …), `label`, `description`, `required`, `default`, `options` for dropdown/radio fields, the author's `validations`, a `visible` rule for conditional fields, the row shape under `item` for arrays, and the embedded tags under `template` for template passages.
- **`automaticFields`** — keys that exist in the document but are filled for you (`global` from the account, `hidden` from the field's default, `computed` from other fields). The assistant must not send values for these; doing so is rejected.
```json
{
"document": {
"id": "6a394ed08ee8c3669f2c3e17",
"name": "Offer letter.docx",
"format": "docx",
"version": {
"id": "6a394ed08ee8c3669f2c3e1a",
"fields": [
{ "path": "name", "type": "string", "subtype": "dropdown", "label": "Name", "required": true,
"options": [{ "value": "inam", "label": "Inam" }, { "value": "andreas", "label": "Andreas" }] },
{ "path": "role", "type": "string", "label": "Role", "required": false },
{ "path": "start_date", "type": "string", "subtype": "date", "label": "Start date", "required": false },
{ "path": "is_senior", "type": "boolean", "label": "Is senior", "required": false },
{ "path": "terms", "type": "array", "label": "Terms",
"item": { "type": "object", "fields": [ { "path": "title", "type": "string" }, { "path": "description", "type": "string" } ] } }
],
"automaticFields": [
{ "path": "company_name", "source": "global" }
]
}
}
}
```
Abbreviated — every field carries the full set of keys. The REST page has the complete shape and the [rules for each type](https://docs.nextsign.dk/docs/v3/documents-get#what-to-send-by-type).
A `pdf` document has an empty `fields` list: there is nothing to fill, and it is attached as it is.
`404 document-not-found` means the document is not yours to use — another company's, a colleague's private one, or an id that does not exist. The assistant should go back to [`list_documents`](https://docs.nextsign.dk/docs/mcp/list-documents) rather than retry.
## Related
### create_case_from_document
Source: https://docs.nextsign.dk/docs/mcp/create-case-from-document
Summary: Every field the create_case_from_document tool accepts — fill a library document and send it for signing, the same as POST /v3/api/documents/{id}/cases.
Creates a case whose document is one from your library, filled with the values in `tags`, and — unless it is a draft — sends it. It accepts **the same payload as [`POST /v3/api/documents/{id}/cases`](https://docs.nextsign.dk/docs/v3/documents-create-case)** (with the document id as an argument instead of in the URL) and runs through the same handler as [`create_case`](https://docs.nextsign.dk/docs/mcp/create-case) — recipients, settings, sender and the returned case are identical.
The schema is **strict**: unknown fields are rejected, inside `tags` as well, and every problem in a payload comes back at once, each with a `field` path.
The assistant should call [`get_document`](https://docs.nextsign.dk/docs/mcp/get-document) first and build `tags` from its `fields`. The tool rejects unknown keys, values for automatic (global, hidden, computed) fields, wrong types, and missing required fields — so a guessed payload fails loudly rather than producing a document with blanks.
## Top-level fields
| Field | Type | Notes |
| ----- | ---- | ----- |
| `id` | ObjectId | **Required.** The document id, from [`list_documents`](https://docs.nextsign.dk/docs/mcp/list-documents) |
| `title` | string, 1–300 | **Required** |
| `tags` | object | Values for the document's fields, keyed by `path`. See [Tags](#tags) |
| `versionId` | ObjectId | Pin a specific version of the document. Default: the current version |
| `signObligated` | boolean | The document must be signed. Default `true`; `false` attaches it for reading only |
| `documentMustBeRead` | boolean | Must be opened before signing. Default `false` |
| `signatories` | int array | Indexes into `recipients` — which of them sign. Default: all signers |
| `recipients` | array ≤ 100 | **Required non-empty when `state` is `open`**. Same shape as in [`create_case`](https://docs.nextsign.dk/docs/mcp/create-case#recipient) |
| `state` | `open` \| `draft` | Default `open`. `draft` saves the case with the values still editable in the dashboard |
| `referenceId` | string ≤ 200 | Your own external reference |
| `folder` | string ≤ 200 | Folder name in the dashboard |
| `senderId` | ObjectId | Attribute the case to this company member (wins over `senderEmail`) |
| `senderEmail` | email | Attribute by address instead |
| `messageTemplateId` | ObjectId | Message template for the invitation |
| `presetId` | ObjectId | Preset that fills unset settings (and overrides locked fields) |
| `settings` | object | Same as in [`create_case`](https://docs.nextsign.dk/docs/mcp/create-case#settings) — defaults apply even when omitted |
There is no `documents` array and no document `name` — the document is the one named by `id`, and the attached file keeps the library version's own name.
## Tags
An object keyed by each field's `path` from `get_document`. Nested fields are nested objects, lists are arrays of rows, a `template` passage is a string or `{ "text", "values" }`:
```json
{
"tags": {
"employee_name": "Andreas Lauridsen",
"start_date": "2026-09-01",
"weekly_hours": 37,
"address": { "city": "Aarhus", "zip": "8000" },
"other_benefits": [{ "name": "Phone", "value": "Paid by employer" }]
}
}
```
| Field type | Send |
| ---------- | ---- |
| `string` | a string; for `date` something `Date` can parse (ISO 8601 is safest); with `options`, one of the `options[].value` strings |
| `number` | a JSON number, not a numeric string |
| `boolean` | `true`/`false`; a required boolean must be `true` |
| `array` | rows shaped like `item` |
| conditional (`visible` set) | only when its rule holds against the other values — otherwise leave it out |
| automatic (`automaticFields`) | nothing — these are filled from the account, the field's default, or other fields |
Only a `docx` has fields. Sending any `tags` key for a `pdf` document is rejected.
## Example call
```json
{
"id": "6a394ed08ee8c3669f2c3e17",
"title": "Offer letter — Andreas Lauridsen",
"recipients": [
{ "name": "Andreas Lauridsen", "email": "al@example.com" }
],
"tags": {
"name": "andreas",
"role": "Engineer",
"start_date": "2026-09-01",
"is_senior": true,
"terms": [{ "title": "Pay", "description": "Monthly, in arrears" }]
},
"settings": { "expiresInDays": 14 }
}
```
## Result
The tool returns JSON. On success, the same `case` object as [`create_case`](https://docs.nextsign.dk/docs/mcp/create-case#result) and the REST endpoints — id, state, recipients with their signing links, the rendered document, delivery status and any notices. The `.docx` is filled and rendered to PDF, so the attached document is a `.pdf`.
On failure the result is marked as an error and explains itself, with paths relative to the call:
```json
{
"error": "validation-failed",
"details": [
{ "field": "tags.nope", "message": "unknown field" },
{ "field": "tags.company_name", "message": "is a global field and is filled from the account" },
{ "field": "tags.name", "message": "must be one of: inam, andreas" },
{ "field": "tags.employee_name", "message": "is required" }
]
}
```
`document-not-found` means the document is not the connection's to use (see [What you see](https://docs.nextsign.dk/docs/v3/documents-list#what-you-see)); a `versionId` that belongs to another document is a `validation-failed` on `versionId`.
A call that does not match the schema at all — a missing `title`, say — is rejected by the protocol layer as an *invalid params* error before the tool runs; same information, different envelope.
**Asking twice creates two cases.** There is no idempotency key. If the assistant reports an error or a timeout, check the dashboard before asking again — see [Troubleshooting](https://docs.nextsign.dk/docs/mcp/troubleshooting#duplicate-cases).
## Related
## Help
Common errors, what they mean, and current limitations.
### Troubleshooting
Source: https://docs.nextsign.dk/docs/mcp/troubleshooting
Summary: Common errors when connecting or creating cases, and the current limitations of the beta.
## Connecting fails
| Symptom | Meaning | Fix |
| ------- | ------- | --- |
| Sign-in window says the request **expired** | The consent link is valid for 10 minutes | Start the connection again from the assistant |
| `401` with `invalid-token` | The key behind the connection does not exist | Reconnect |
| `401` with `key-revoked` / `key-expired` | The connection's key was deleted in the dashboard, or aged out | Reconnect, or create a new key ([Authorization](https://docs.nextsign.dk/docs/v3/authorization)) |
| `403` with `company-disabled` | The company account is disabled | Contact support |
| `405 Method not allowed` | The client tried the older SSE transport | Use a client that supports **Streamable HTTP** |
| Client can't authenticate at all | The client supports neither OAuth nor custom headers | Switch clients, or bridge with `mcp-remote` ([Connect](https://docs.nextsign.dk/docs/mcp/connect)) |
A connection is just an API key (see [Connect](https://docs.nextsign.dk/docs/mcp/connect#managing-connections)) — deleting it in the dashboard takes effect on the assistant's next request, so an assistant that suddenly reports authentication errors was likely disconnected there.
## `key-has-no-user`
Only affects API-key connections. Creating a case acts on behalf of a person, and some keys (typically partner-issued ones) have no person behind them. Use a key created from the dashboard by the user the assistant should act as — or connect with OAuth, where this cannot happen.
## "The case was rejected"
The assistant sent something the API refused. All problems come back at once, each naming the field. The usual causes:
- An **open** case with no recipients or no documents — add them, or ask for a `draft`.
- An SMS recipient without a **phone number**.
- A document with more than one source — exactly one of a link, file contents, or a library id.
- A file over **20 MB**.
- A misspelled or unknown field — the schema rejects unknown keys rather than ignoring them.
- For a case from a **library template**: a tag the template does not have, a value for an automatic field (company name, date, sender), a wrong type, a dropdown value that is not one of the options, or a **required field left empty**. The assistant should re-read the fields with `get_document` and ask you for what is missing.
The assistant sees the same explanation you do, so "fix it and try again" usually works — but see the next section first.
## Duplicate cases
There is no idempotency key yet: retrying a request that actually succeeded creates a **second case** and can email your signers twice.
After any error, timeout, or "did that work?" moment — check the dashboard before asking the assistant again. If a duplicate slipped through, delete it in the dashboard.
## Beta limitations
- **Four tools.** Creating cases, and listing, reading and creating cases from your document library. No listing, updating or deleting of *cases* over MCP yet (the [REST API](https://docs.nextsign.dk/docs/v3) has more), and no uploading to the library — new templates are added in the dashboard.
- **No idempotency key.** See above.
- **No rate limiting on the MCP endpoint yet.** Don't treat that as a licence to hammer it; a limit is coming.
- **Scopes are not enforced.** Every connection currently has full access to its company. Grants are already recorded with `cases:write`; the document tools will require `documents:read` once enforcement arrives, and today's connections keep working.
- **`publicUrls`** (REST query flag) is not available over MCP.
## More
Additional reference pages.
### Company Status
Source: https://docs.nextsign.dk/docs/company-status
Summary: Retrieve your remaining credit balance and credit usage using GET /v3/company/{company}/status.
Use this endpoint to check the status of your NextSign account: how many credits you have left, and how many credits you have used and purchased — in total or within a given period.
This is useful for monitoring your balance from your own systems, e.g. to alert you before you run out of credits.
## Authorization
All requests to this endpoint require authentication using a **Bearer Token**.
Use your NextSign API key as the bearer token for this endpoint. You create API keys under Dashboard → API keys.
---
## Endpoint
```text
GET https://api.nextsign.dk/v3/company/{company}/status
```
## Path Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | `string` | Your company identifier |
## Query Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `from` | `string` | Optional. ISO 8601 date. Only count usage on or after this date |
| `to` | `string` | Optional. ISO 8601 date. Only count usage on or before this date |
`from` / `to` only affect the usage numbers (`used`, `added`, `usedByItem`). `remaining` is always your current balance.
## Example Request
```bash title="cURL"
curl --location 'https://api.nextsign.dk/v3/company/{company}/status?from=2026-01-01' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
## Example Response
```json title="200 OK"
{
"company": {
"id": "6576e19662ec13c4faa7de7b",
"name": "Example ApS",
"state": "active"
},
"credits": {
"remaining": 42,
"used": 158,
"added": 200,
"usedByItem": {
"signing": 130,
"form-submission": 20,
"phone-recipient": 8
}
},
"period": {
"from": "2026-01-01",
"to": null
}
}
```
## Response Fields
| Field | Type | Description |
| ----- | ---- | ----------- |
| `company.id` | `string` | Company identifier |
| `company.name` | `string` | Company name |
| `company.state` | `string` | Account state: `active` or `disabled` |
| `credits.remaining` | `number` | Credits currently left on the account |
| `credits.used` | `number` | Credits spent (within the period, if one is given) |
| `credits.added` | `number` | Credits added — purchases, trial credits, refunds, etc. (within the period, if one is given) |
| `credits.usedByItem` | `object` | Credits spent per item type, e.g. `signing`, `form-submission`, `phone-recipient`, `eboks-recipient` |
| `period` | `object` | Echo of the `from` / `to` filter. Only present when a filter is given |
## Common Error Responses
Invalid date filter:
```json title="400 Bad Request"
{
"error": "'from' and 'to' must be valid dates (ISO 8601)"
}
```
## Related Endpoints
- [Authorization](https://docs.nextsign.dk/docs/authorization)
- [Rate Limits](https://docs.nextsign.dk/docs/rate-limits)
### Get a Webflow
Source: https://docs.nextsign.dk/docs/v3/webflows-get
Summary: Retrieve webflow progress, recipient links and non-sensitive answers.
```http
GET https://api.nextsign.dk/v3/api/webflows/{id}
```
Retrieve a webflow belonging to the API key's company. This also works for webflows started in the dashboard. Deleted webflows are not returned.
```bash
curl 'https://api.nextsign.dk/v3/api/webflows/65ab12cd34ef56ab78cd90b1' \
--header "Authorization: Bearer $NEXTSIGN_API_KEY"
```
The response is `200 OK` with the same `{ "webflow": ... }` envelope as [Send a Webflow](https://docs.nextsign.dk/docs/v3/webflows-create#response).
| Field | Meaning |
| --- | --- |
| `id`, `formId` | Webflow ID and original form ID. |
| `title`, `referenceId` | Title and your reference (`null` if absent). |
| `state` | `awaiting-responses` or `completed`, based on the webflow's submitted state. This is not the signing status. |
| `createdAt`, `completedAt` | ISO timestamps; completion time is `null` until recorded. |
| `caseId` | Resulting signing case ID when available, otherwise `null`. Use [Get a Case](https://docs.nextsign.dk/docs/v3/case-get) for signing progress. |
| `recipients` | Recipient progress and answers as detailed below. |
Each recipient contains `id`, `role`, `name`, `email`, `requiresResponse`, `completed`, `invitationStatus`, `formUrl` and `answers`. `formUrl` is `null` when the recipient is complete or is not required to answer. Dashboard-created flows have `not-requested` as their API invitation status; that does not mean the dashboard never sent an email.
## Answers
Answers are returned only after the recipient is marked complete, and only for declared fields assigned to that recipient in the saved form snapshot. Before completion, `answers` is empty; prefilled values are not themselves submitted answers. Sensitive/encrypted answers are redacted, even after completion:
```json
[
{ "tag": "phone", "value": "12345678" },
{ "tag": "customer_cpr", "redacted": true }
]
```
An empty array means no eligible answers are stored. Raw form snapshots, encryption ciphertext, CPR metadata and internal credentials are not included. Personal form links and non-sensitive answers should still be handled as private customer data.
Poll with a reasonable interval and back off after errors. This API addition does not introduce a webflow-completion webhook.
## Errors
An invalid ID returns `400 validation-failed`. An unknown ID, another company's webflow, or a deleted webflow returns `404 webflow-not-found`. See [Authorization](https://docs.nextsign.dk/docs/v3/authorization) for authentication failures.
### Migrating from v2
Source: https://docs.nextsign.dk/docs/v3/migration
Summary: Field-by-field mapping from POST /api/v2/{company}/case/create to POST /v3/api/cases, and every behavioural difference.
This page maps [v2 Case Create](https://docs.nextsign.dk/docs/case-create) onto [v3 Create a Case](https://docs.nextsign.dk/docs/v3/case-create) and lists the behaviour that differs.
**v2 is not deprecated and is not changing.** You can migrate case creation to v3 and keep using v2 for listing, updating, forms, documents and webhooks — both surfaces read and write the same data.
## Start here
Three changes will affect almost every existing payload:
1. **Unknown fields are rejected.** Anything v2 quietly ignored is now a `400`. Expect your first v3 request to fail, and read the `details` array — it names every field at once.
2. **`recipients[].signing` now defaults to `true`.** In v2 it defaults to `false`. If you relied on that, set `"signing": false` explicitly on observers.
3. **Cases expire in 30 days by default, not 90.** Send `settings.expiresInDays` if you depended on v2's default.
## Endpoint and envelope
| | v2 | v3 |
| --- | --- | --- |
| URL | `POST /api/v2/{company}/case/create` | `POST /v3/api/cases` |
| Host | `https://www.nextsign.dk` | `https://api.nextsign.dk` |
| Company | path, body, or query | from the API key |
| Body wrapper | `body` or `body.values` both accepted | `body` only |
| Success | `200` `{ data, notices }` | `201` `{ case }` + `Location` header |
| Failure | `{ errors: [{ message }] }`, sometimes with `200` | `{ error, details }`, always `4xx` |
## Field mapping
### Case
| v2 | v3 | Note |
| --- | --- | --- |
| `title` or `name` | `title` | The `name` alias is gone |
| `referenceId` | `referenceId` | |
| `type` | *not accepted* | Every v3 case is `Simple sign` |
| `autoSend` | `settings.autoSend` | |
| — | `state` | New: `draft` creates without sending |
| `folder` | `folder` | Now rejects a name that does not exist |
| `roles` | *removed* | Always taken from the folder |
| `settings.lang` | `settings.language` | |
| `settings.availability.days` | `settings.expiresInDays` | **Default changed: 90 → 30** |
| `settings.reminders.send` | `settings.reminders.send` | |
| `settings.reminders.amount` | `settings.reminders.amount` | Now bounded `1`–`20` |
| `settings.reminders.daysBetween` | `settings.reminders.daysBetween` | Now bounded `1`–`365` |
| `reminderAutoSend` | `settings.reminders.autoSend` | |
| `settings.deletion` | *not available* | Company default applies |
| `settings.allowRecipientsViewOthers` | *not available* | Company default applies |
| `template` | `settings.template` | |
| `emailtemplate` or `emailTemplate` | `messageTemplateId` | An unknown id now fails the request |
| `customMessage` + `message` | `settings.message` | Sending `settings.message` is enough |
| `user` | `senderId` | Now verified to be a company member |
| `user_email` | `senderEmail` | Still looks a user up, but they must be a company member |
| `user_name` | *not accepted* | The display name comes from the sender's profile |
| `signingSchemas` | `settings.signingSchemas` | Unknown values now rejected |
| `tags` | `tags` | |
| `signingPage.logo` | `settings.logo` | |
| `integrations.microsoft.returnPath` | `settings.integrations.microsoft.returnPath` | Must now begin with `/` |
| `realtime` | *not available* | |
| — | `presetId` | New: applies your company's preset |
### Recipients
| v2 | v3 | Note |
| --- | --- | --- |
| `group` or `order` | `group` | The `order` alias is gone |
| `signing` | `signing` | **Default changed: `false` → `true`** |
| `template` | `messageTemplateId` | |
| `type: "phone"` | `type: "sms"` | |
| everything else | unchanged | |
### Documents
| v2 | v3 |
| --- | --- |
| `file` + `fileIsBlob: true` | `content` |
| `file` + `fileIsBlob: false` | `url` |
| `document_id` | *not accepted* |
| — | `documentId` + `versionId` — attach from your document library |
| `name` optional | `name` **required** |
## Behaviour differences
### Things that will change your results
**Cases expire in 30 days by default**, not 90. v2's 90 was an API-only outlier; every other part of NextSign uses 30.
**`recipients[].signing` defaults to `true`.** Omitting it in v2 creates a case nobody can sign, with no warning.
**The signing page language follows your company**, then falls back to Danish. v2 always used Danish for API-created cases regardless of company settings.
**Sequential signing works.** v2 never set the group cursor on API-created cases, so group ordering did not gate correctly. v3 sets it, and it is returned as `activeGroup`.
**An unknown folder is an error.** v2 silently files the case in the default folder — which has a different role list, so the case ends up visible to a different set of colleagues.
**Unrecognised file content is rejected — when you send the bytes.** v2 stored anything it could not identify as a PDF, so a JPEG became a "PDF" that broke the signing page later. v3 inspects `content` and rejects anything that is not a PDF or a real `.docx`. A document sent as a `url` is still stored by reference and not inspected, in v3 as in v2.
**`roles` cannot be sent.** In v2 a client-supplied `roles` array widens who inside your company can see the case, bypassing the folder's access list.
**Sender attribution never guesses silently.** With no user, v2 assigns the case to whichever company user sorts first. v3 uses the user you name in `senderId` or `senderEmail`; with neither, it falls back to the person who created the key and tells you so in `notices`. A key with no user behind it is rejected outright.
**You cannot set a sender name or email freely.** v2 takes `user_name` and `user_email` straight from the payload, so a case can claim to come from any address. In v3 those values are read from the named user's NextSign profile.
### Things that make failures easier to handle
**A `4xx` means nothing was created.** v2 can return `500` for a case that exists and has already been emailed, because sending happens after the save and is not isolated — retry that and you get a duplicate. v3 has no such window; see [Errors](https://docs.nextsign.dk/docs/v3/errors).
**Send failures are reported, not thrown.** A failed email comes back as `delivery.email.ok: false` on a `201`.
**One contact failure no longer fails the case.** v2 aborts the whole creation if a recipient cannot be written to your contact list.
**All validation errors arrive together**, each with a `field` path.
### Response differences
| | v2 | v3 |
| --- | --- | --- |
| Case id | `data._id` | `case.id` |
| Signing link | `data.recipients[].url` | `case.recipients[].signingUrl` |
| Recipient handle | `data.recipients[].uid` | `case.recipients[].uid` — and `case.recipients[].id` is its `_id` |
| Document link | `data.documents[].file` | `case.documents[].url` |
| Signing status | `data.recipients[].signed` | `case.recipients[].status` |
| Send outcome | not reported | `case.delivery` |
v3 returns a defined object rather than the raw database document, so `signingToken`, `nextSignKey`, encrypted `cpr`, `signer`, `logs`, `events`, `roles` and `__v` are no longer present.
If your integration reads `signingToken` to build signing links itself, switch to `signingUrl`. Hand-built links break when the signing domain, language prefix or template number changes — all three are resolved per case on our side.
## Worked example
v2:
```json
{
"title": "Lejeaftale",
"user_email": "info@example.com",
"settings": {
"lang": "da",
"availability": { "days": 14 },
"reminders": { "send": true, "amount": 2, "daysBetween": 3 }
},
"recipients": [
{ "name": "Andreas", "email": "al@example.com", "signing": true, "order": 0 }
],
"documents": [
{ "name": "Lejekontrakt.pdf", "file": "JVBERi0...", "fileIsBlob": true }
]
}
```
v3:
```json
{
"title": "Lejeaftale",
"senderEmail": "anna@example.com",
"settings": {
"language": "da",
"expiresInDays": 14,
"reminders": { "send": true, "amount": 2, "daysBetween": 3 }
},
"recipients": [
{ "name": "Andreas", "email": "al@example.com", "group": 0 }
],
"documents": [
{ "name": "Lejekontrakt.pdf", "content": "JVBERi0..." }
]
}
```
`user_email` in v2 is a display value. `senderEmail` in v3 looks up a colleague, so it has to be the address of a NextSign user in your company — a generic `info@` mailbox that nobody signs in with is rejected with `sender-user-not-found`.
## Suggested rollout
### Create a separate key
Mint a new API key for the v3 integration, so you can revoke it without touching anything else.
### Send your existing payload
Post it to `/v3/api/cases` in the [test environment](https://docs.nextsign.dk/docs/testenv) and read the `details` array. Every wrong field is named at once.
### Rename until it passes
Then compare the created case in the dashboard against one made through v2 — folder, sender, signing order, documents.
### Move production traffic
Keep the v2 path available to fall back to until you are confident.
## Related
### Send a Webflow
Source: https://docs.nextsign.dk/docs/v3/webflows-create
Summary: Request information using a published webflow and email invitations.
Create a webflow from a published form and invite people to supply information, just as you do in the dashboard. The form defines the pages, questions, recipient roles, identity checks and what happens after completion. Creating a webflow does not immediately create a signing case.
```http
POST https://api.nextsign.dk/v3/api/webflows
```
## Before you start
Publish a form with webflow enabled in the dashboard. Use its form ID and the stable `tag` identifier of each recipient role, not the role's display name (`tagName`). Include every role exactly once, including roles that only sign later. At least one role must supply information.
Use an [API key](https://docs.nextsign.dk/docs/v3/authorization) created by a user who is still a member of your company. That user is the sender. Forms and email templates must belong to the same company as the key. Unpublished changes are not used: each webflow stores a snapshot of the published form.
This endpoint sends **email invitations** to roles configured to supply information. SMS and other initial delivery channels are not supported by this endpoint.
## Quickstart
Replace the form ID and role identifiers with values from your published form. This example assumes a form with one role, `customer`, and a field named `order_number`.
```bash
curl --request POST 'https://api.nextsign.dk/v3/api/webflows' \
--header "Authorization: Bearer $NEXTSIGN_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-1042-information-v1' \
--data '{
"formId": "65ab12cd34ef56ab78cd90b0",
"title": "Information for order 1042",
"referenceId": "order-1042",
"recipients": [
{ "role": "customer", "name": "Alex Example", "email": "alex@example.com" }
],
"values": { "order_number": "1042" }
}'
```
Store the idempotency key alongside the operation in your system. Reuse that key and the same payload after a timeout or network failure.
## Request
Unknown properties are rejected. Do not send company IDs, sender overrides, page definitions or uploaded document content here; configure the published form in the dashboard.
| Property | Required | Meaning |
| --- | --- | --- |
| `formId` | Yes | Published webflow form ID (24 hexadecimal characters). |
| `title` | No | Webflow title, 1–300 characters. Defaults to the form title. |
| `referenceId` | No | Your external reference, up to 200 characters. Does not replace the idempotency key. |
| `recipients` | Yes | 1–100 recipients, exactly one for every configured role. |
| `emailTemplateId` | No | Company email template for information requests. |
| `values` | No | Initial values keyed by existing form tag identifiers. Defaults to an empty object. |
Each recipient accepts:
| Property | Required | Meaning |
| --- | --- | --- |
| `role` | Yes | Stable `tag` identifier of the form's recipient role. |
| `name` | Yes | Recipient name, 1–200 characters. |
| `email` | Yes | Valid email address. |
| `phone` | No | Phone number, up to 40 characters; this does not enable SMS delivery. |
| `cpr` | When required by the role | Exactly ten digits. Stored encrypted. |
| `emailTemplateId` | No | Override the invitation email template for this recipient. |
Template precedence is: recipient override → request override → role template → form's default invitation template → standard invitation. These are information-request emails, not the signing case's email templates.
`values` accepts strings (up to 20,000 characters), finite numbers, booleans and `null`. Arrays and objects are not accepted. Unknown tags are rejected. These values prefill the form; they do not mark questions answered or recipients complete. Sensitive/encrypted fields are encrypted in the saved webflow. Field rules still apply during form completion.
## Response
First creation returns `201 Created` with `Location: /v3/api/webflows/{id}`. A replay returns `200 OK` with the same webflow's current status.
```json
{
"webflow": {
"id": "65ab12cd34ef56ab78cd90b1",
"formId": "65ab12cd34ef56ab78cd90b0",
"title": "Information for order 1042",
"referenceId": "order-1042",
"state": "awaiting-responses",
"createdAt": "2026-09-23T12:00:00.000Z",
"completedAt": null,
"caseId": null,
"recipients": [
{
"id": "65ab12cd34ef56ab78cd90b2",
"role": "customer",
"name": "Alex Example",
"email": "alex@example.com",
"requiresResponse": true,
"completed": false,
"invitationStatus": "submitted",
"formUrl": "https://app.nextsign.dk/webflow/65ab12cd34ef56ab78cd90b3/65ab12cd34ef56ab78cd90b1/65ab12cd34ef56ab78cd90b2",
"answers": []
}
]
}
}
```
The `formUrl` host follows your environment. Treat each URL as a personal access credential: do not publish it or share it with another recipient. See [Get a Webflow](https://docs.nextsign.dk/docs/v3/webflows-get) for response fields and answer retrieval.
## Retries and invitation status
`Idempotency-Key` is required: 1–128 printable ASCII characters without spaces, unique per operation within your company. The same key and payload reuse the existing webflow and **never resend invitations**, including concurrent retries. A changed payload with the same key returns `409 idempotency-conflict`. A new key creates a new webflow, even when `referenceId` is unchanged.
| `invitationStatus` | Meaning |
| --- | --- |
| `pending` | No dispatch outcome has been recorded. A concurrent request may still be sending, or processing may have stopped. |
| `submitted` | The invitation helper completed successfully. This is not proof of inbox delivery. |
| `unknown` | The dispatch attempt raised an error; the email may or may not have been sent. |
| `not-requested` | No API invitation attempt was recorded, for example for a role that only signs. |
A newly created webflow returns `201` even if a send attempt has an unknown outcome. Inspect every recipient's status. Fetch the webflow again to check pending attempts; a replay does not retry them. Resolve uncertain delivery from the dashboard, or use the personal link through your own delivery process. Do not switch keys merely to retry an invitation.
A server error or timeout can occur after persistence. Retry with the **same key and body** to recover the webflow safely. Keep the key for the lifetime of the operation.
## Errors
| Status | Error | Meaning |
| --- | --- | --- |
| `400` | `validation-failed` | Invalid body/key, mismatched roles, unknown tags, or an unknown email template. See `details`. |
| `403` | `key-has-no-user` | The key has no sender; use a dashboard-created key. |
| `403` | `sender-not-in-company` | The key's sender is no longer a company member. |
| `404` | `form-not-found` | No published, webflow-enabled form with this ID in your company. |
| `409` | `idempotency-conflict` | This key already belongs to a different payload. No additional webflow was created. |
Standard authentication failures are described under [Errors](https://docs.nextsign.dk/docs/v3/errors). Per-key scopes are not enforced yet, as described under [Authorization](https://docs.nextsign.dk/docs/v3/authorization).
### White-Label Partner Program
Source: https://docs.nextsign.dk/docs/partner-program
Summary: Offer e-signing that looks like your own, on your domain and brand. Refer customers, earn a kickback, and get an API key for every new account.
The partner program lets an existing NextSign company refer new customers. Customers who sign up through your referral link are linked to your company for good. From then on they can get a discount on credits, you earn a kickback on what they spend, and if you integrate NextSign for your customers, you can receive an API key for each new account through a webhook.
With full white-labelling, signing pages, signing links and emails carry your own domain and brand, so the signing flow looks like your own solution. See [White-Labelling](#white-labelling).
## How It Works
### NextSign sets you up as a partner
Partner status and the commercial terms are configured by NextSign on your company. There is no self-service signup and no API for it. Contact NextSign to become a partner.
### You share your referral link
Copy the link from **Dashboard → Partners** and share it with the companies you refer.
### The customer signs up
The signup page shows your company's name and logo. When the account is created, the new company is permanently attributed to yours.
### You receive the new account (optional)
If the integration webhook is enabled for you and the customer consents, NextSign creates an API key for the new account and sends it to your webhook.
### Credit purchases earn a kickback
Every credit purchase the customer makes is discounted by your agreed discount, and your agreed kickback is recorded to your company.
---
## Partner Configuration
Your terms are stored on your company and are set by NextSign.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `isPartner` | `boolean` | Whether the company is an active partner. Discounts, kickbacks and the webhook only apply while this is `true` |
| `code` | `string` | Optional friendly referral code, such as `acme`. Without one, your company ID is used in the link |
| `kickbackPercent` | `number` | Your share of what referred customers pay, as a fraction: `0.15` means 15% |
| `discountPercent` | `number` | The discount referred customers get on credits, as a fraction: `0.10` means 10% |
| `webhookEnabled` | `boolean` | Whether you can receive the `partner.customer.registered` webhook |
The percentages are read live at the time of each purchase. They are not locked in when a customer signs up. If your terms change, the new percentages apply to the next purchase of every customer you have referred. Purchases already recorded stay as they are.
---
## Referral Link
```text
https://www.nextsign.dk/user/register?ref=
```
`ref` accepts either your referral `code` or your company ID. The link in **Dashboard → Partners** uses your code if you have one. For signups in another language, the dashboard adds a locale prefix, for example `/en/user/register?ref=acme`.
### Co-branded signup
The signup page looks up the `ref` value with a public endpoint and, if it belongs to an active partner, shows your name and logo:
```text
GET https://api.nextsign.dk/v3/public/partner/:ref
```
```json title="200 OK (active partner)"
{
"isPartner": true,
"name": "Acme ApS",
"logo": "...",
"webhookEnabled": true
}
```
For an unknown `ref`, or for a company that is not an active partner, the endpoint returns `200` with `{ "isPartner": false }` and the signup page looks like a normal signup.
### Attribution
Attribution happens once, when the account is created through the link. It is permanent and is never moved to another company.
- It is set even if the referring company is not a partner yet, so customers you refer before you become a partner count once you do.
- An existing NextSign company cannot be attached to you afterwards by opening your link.
- Every signup through a referral link also gives the referring company 25 free credits. This is the general refer-a-friend perk and applies to partners and non-partners alike.
---
## Discount and Kickback
Only **credit purchases** count, whether the customer buys credits manually or through auto-refill by card, invoice or EAN.
- **Discount:** the customer pays the credit price reduced by `discountPercent`. At card checkout it shows as a *Partner referral discount* coupon. Auto-refills are charged at the discounted price directly.
- **Kickback:** you earn `kickbackPercent` of the amount the customer actually paid, excluding VAT and after the discount.
**Example:** with a 10% discount and a 15% kickback, a customer buys credits listed at 1,000 DKK excl. VAT.
| | Amount |
| - | ------ |
| Customer pays (excl. VAT) | 900 DKK |
| Customer's saving | 100 DKK |
| Your kickback (15% × 900 DKK) | 135 DKK |
Kickbacks are recorded as a balance owed to your company, once per invoice. Payout is handled manually by NextSign.
### Partner dashboard
**Dashboard → Partners** shows your referral link, your current percentages, and for each referred customer their signup date, spend, the kickback you have earned and the discount they have received.
---
## Integration Webhook
If you integrate NextSign for your customers, you can receive an API key for every new account the moment it signs up through your link. This feature is off by default. Ask NextSign to turn on `webhookEnabled` for your company.
### Set up the webhook
Go to **Dashboard → Webhooks** and create a webhook with the trigger **New referred customer** (`partner.customer.registered`). The trigger only appears in the list for partners with `webhookEnabled`. The URL, method, active toggle and custom headers work as described in [Webhooks](https://docs.nextsign.dk/docs/webhooks).
### Customer consent
When `webhookEnabled` is on, the signup page shows an extra checkbox the customer must tick before continuing:
> I accept that NextSign creates and sends integration details to *your company name*.
The key is only created and sent when all of the following are true:
- the customer signed up through your referral link and ticked the consent box
- your company is an active partner with `webhookEnabled`
- you have at least one active webhook with the `partner.customer.registered` trigger
If you have no matching active webhook, no key is created.
### Request
NextSign sends one request to each matching webhook, using the method you configured (`POST` by default) and your custom headers. If you do not set `Content-Type` yourself, NextSign sends `application/json`.
```json title="Payload"
{
"trigger": "partner.customer.registered",
"company": {
"id": "66263484e601d81006d1a132",
"name": "Sample Customer ApS",
"cvr": "12345678",
"apiKey": "key_Wb22OsfMPpEkAHeS6LHqBWoaFkpEuCdyZx3pt5RvICo"
}
}
```
| Field | Type | Description |
| ----- | ---- | ----------- |
| `trigger` | `string` | Always `partner.customer.registered` |
| `company.id` | `string` | The new customer company's ID |
| `company.name` | `string` | The company name entered at signup |
| `company.cvr` | `string` | The company's CVR number |
| `company.apiKey` | `string` | A new API key for the customer's account, in full |
The payload contains exactly these fields. Nothing else is sent.
### Delivery
This webhook is delivered differently from case and flow webhooks:
- **One attempt only.** There is no retry, and the key is never sent again. If your endpoint is down or returns an error, the key is lost to you.
- **No signature.** NextSign does not sign the request. To verify that it came from NextSign, configure a shared secret as a custom header, such as `X-Webhook-Secret`, and check it in your handler.
- **Not in delivery runs.** Deliveries of this trigger do not appear in the webhook's recent runs in the dashboard.
- **Signup waits for you.** The request is sent while the customer's account is being created, so a slow endpoint makes their signup slow.
Respond with a `2xx` status as fast as you can. Store the payload first and do any slow work, such as provisioning on your side, in a background job afterwards.
If a delivery is lost, the customer still has the key in their account, but the full key cannot be shown again. Ask the customer to create a new key under **Dashboard → Config → API Keys** and share it with you, and to revoke the unused one.
### Use the key
The key belongs to the customer's company. Use it like any other API key, with `Authorization: Bearer` against `https://api.nextsign.dk/v3/api/*`:
```bash title="cURL"
curl https://api.nextsign.dk/v3/api/cases \
-H "Authorization: Bearer $CUSTOMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Rental agreement",
"recipients": [{ "name": "Andreas Lauridsen", "email": "al@example.com" }],
"documents": [{ "name": "contract.pdf", "content": "JVBERi0xLjQK..." }]
}'
```
See [Create a Case](https://docs.nextsign.dk/docs/v3/case-create) for the full request reference.
- **Full access.** The key is created as the person who signed up, who is an admin of the new company, so it can do everything that person can.
- **Attributed to the person who signed up.** Cases created with the key come from that person unless you name another sender. See [Who your cases belong to](https://docs.nextsign.dk/docs/v3/authorization#who-your-cases-belong-to).
- **Visible to the customer.** The key appears in the customer's API keys as *Partner integration (your company name)*. It does not expire, but the customer can revoke it at any time, after which requests fail with `401 key-revoked`.
Treat the key as a secret. It gives full access to your customer's account. Store it encrypted, keep it out of logs and client-side code, and never pass it on.
### Example handler
```javascript title="Node.js / Express"
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/nextsign/partner', async (req, res) => {
if (req.headers['x-webhook-secret'] !== process.env.NEXTSIGN_WEBHOOK_SECRET) {
return res.status(401).send('Unauthorized');
}
const { trigger, company } = req.body;
if (trigger !== 'partner.customer.registered') {
return res.status(200).send('Ignored');
}
// Store the key before responding. It is sent once and never again.
await saveCustomerKey({
companyId: company.id,
name: company.name,
cvr: company.cvr,
apiKey: encrypt(company.apiKey),
});
res.status(200).send('OK');
// Slow provisioning work belongs in a background job.
enqueueCustomerProvisioning(company.id);
});
app.listen(3000);
```
---
## White-Labelling
Combine these to make signing look like part of your own product:
| What | How it looks to the recipient | Where to set it up |
| ---- | ----------------------------- | ------------------ |
| **Own signing domain** | Signing pages open on your domain, such as `sign.yourcompany.com` | **Dashboard → Config → Integrations → Custom signing domain** (paid add-on, set up with a CNAME record) |
| **Own email domain** | Signing emails are sent from your domain instead of a NextSign sender | Email domain add-on (paid), then **Dashboard → Config → Company → DNS** |
| **Own logo and design** | Your logo, colours and styling on the signing pages | **Dashboard → Config → Design** |
| **Neutral document key** | The document key printed on signed PDFs reads *Document Key* instead of *NextSign Document Key* | Part of a full white-label agreement. Contact NextSign |
The signing links the API returns in `signingUrl` automatically use the account's own signing domain once it is active, so an integration needs no changes to benefit from it.
Branding is set per NextSign account. Each customer account has its own logo, design, signing domain and email domain.
## Related