# WPsigner REST API Pack

> Authentication, documents, templates, signers, fields, statistics, webhooks, and examples.

Generated for AI assistants from the official WPsigner docs.
HTML guide: https://docs.wpsigner.com/support/use-with-ai/
Pages in this pack: 8

## Suggested prompt

```text
You are helping me configure and use WPsigner, a self-hosted electronic-signature plugin for WordPress.
Use ONLY the documentation below as your source of truth.
Each page header includes edition: lite, pro, or both. Never invent Pro-only features for Lite.
If something is not covered, say so clearly and ask for the missing detail.
Prefer exact admin menu paths, shortcodes, endpoints, and settings names from the docs.
```

## Contents

1. [REST API Overview](https://docs.wpsigner.com/md/api.md) — https://docs.wpsigner.com/api/
2. [Documents API](https://docs.wpsigner.com/md/api/documents.md) — https://docs.wpsigner.com/api/documents/
3. [Code Examples](https://docs.wpsigner.com/md/api/examples.md) — https://docs.wpsigner.com/api/examples/
4. [Fields API](https://docs.wpsigner.com/md/api/fields.md) — https://docs.wpsigner.com/api/fields/
5. [Signers API](https://docs.wpsigner.com/md/api/signers.md) — https://docs.wpsigner.com/api/signers/
6. [Statistics API](https://docs.wpsigner.com/md/api/statistics.md) — https://docs.wpsigner.com/api/statistics/
7. [Templates API](https://docs.wpsigner.com/md/api/templates.md) — https://docs.wpsigner.com/api/templates/
8. [Webhooks](https://docs.wpsigner.com/md/api/webhooks.md) — https://docs.wpsigner.com/api/webhooks/

---

# REST API Overview

> Complete guide to the WPsigner REST API for developers.

edition: pro
Edition: Pro
AI note: This page requires WPsigner Pro. Do not tell Lite users they already have this feature.
HTML: https://docs.wpsigner.com/api/
Markdown: https://docs.wpsigner.com/md/api.md

The WPsigner REST API allows you to interact with your documents, signers, and signing workflows programmatically. Built on top of the WordPress REST API, it provides secure endpoints for managing electronic signatures.

> **Requires WPsigner Pro**
The public REST API and API keys are **Pro** features. [Upgrade to Pro](https://wpsigner.com/pricing/?utm_source=docs&utm_medium=upgrade&utm_campaign=api) · [Lite vs Pro](/getting-started/lite-vs-pro/)

> **Self-hosted: one API per WordPress site**
There is no global API on `wpsigner.com`. After you install the plugin on **your** WordPress site, the API is available at `https://your-site.com/wp-json/insigner/v1/`. Credentials are created in that site's admin panel.

## Base URL

All API endpoints are available at:

```
https://your-site.com/wp-json/insigner/v1/
```

Replace `your-site.com` with the domain where WPsigner is installed and licensed.

## Authentication

WPsigner uses API Key + Secret authentication. You'll need to generate credentials from the WordPress admin panel.

### Generating API Credentials

1. Go to **inSigner → API** in your WordPress admin
2. Click **Create New Key**
3. Enter a name and description for your key
4. Select the permission level:
   - **Full Access**: Read and write operations
   - **Read Only**: Only GET requests allowed
5. Click **Create Key**
6. **Important**: Copy and save the API Secret immediately. It will only be shown once!

### Using API Credentials

Include the following headers in all API requests:

```bash
X-WPS-API-Key: wps_your_api_key_here
X-WPS-API-Secret: your_api_secret_here
```

### Example Request

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/documents" \
  -H "X-WPS-API-Key: wps_PPF0L3qPubG4YedgDx5H5IqMpTgfv5h3" \
  -H "X-WPS-API-Secret: heP9XXeZJJPMttYg4XfRXe6YFvCW..."
```

## Response Format

All responses are returned in JSON format. Successful responses typically include the requested data:

```json
{
  "id": "44",
  "title": "Contract Agreement",
  "status": "sent",
  "created_at": "2024-01-15 10:30:00"
}
```

### Error Responses

Errors follow the WordPress REST API error format:

```json
{
  "code": "rest_forbidden",
  "message": "Invalid API key or secret.",
  "data": {
    "status": 401
  }
}
```

### HTTP Status Codes

| Code | Description |
|------|-------------|
| `200` | Success |
| `201` | Created (for POST requests) |
| `400` | Bad Request - Invalid parameters |
| `401` | Unauthorized - Invalid or missing credentials |
| `403` | Forbidden - Insufficient permissions |
| `404` | Not Found - Resource doesn't exist |
| `429` | Too Many Requests - Rate limit exceeded |
| `500` | Server Error |

## Rate Limiting

To protect your server and ensure fair usage, the API implements rate limiting:

- **Per API Key**: 1,000 requests per hour (configurable per key)
- **Per IP Address**: 100 requests per minute (applies to all REST requests)

When you exceed the rate limit, you'll receive a `429` response (not `401`) with a `Retry-After` header:

```json
{
  "code": "rate_limit_exceeded",
  "message": "API key rate limit exceeded (1000 requests/hour). Please wait 45 seconds.",
  "data": {
    "status": 429,
    "retry_after": 45
  }
}
```

## Permissions

API key permissions determine which operations are allowed:

| Permission | GET | POST | PUT | DELETE |
|------------|-----|------|-----|--------|
| **Full Access** | ✅ | ✅ | ✅ | ✅ |
| **Read Only** | ✅ | ❌ | ❌ | ❌ |

If you attempt a write operation with a Read Only key, you'll receive:

```json
{
  "code": "rest_forbidden",
  "message": "This API key has read-only permissions.",
  "data": {
    "status": 403
  }
}
```

## Available Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/documents` | List all documents |
| `POST` | `/documents/upload` | Upload PDF/PNG/JPG and create a document |
| `POST` | `/documents` | Create from existing secure `file_path` (advanced) |
| `GET` | `/documents/{id}` | Get document details |
| `PUT` | `/documents/{id}` | Update a document |
| `DELETE` | `/documents/{id}` | Delete a document |
| `GET` | `/documents/{id}/signers` | List signers |
| `POST` | `/documents/{id}/signers` | Add a signer |
| `GET` | `/documents/{id}/fields` | List document fields |
| `POST` | `/documents/{id}/fields` | Save document fields |
| `POST` | `/documents/{id}/send` | Send for signing |
| `POST` | `/documents/{id}/remind` | Remind pending/viewed signers |
| `POST` | `/documents/{id}/expire` | Apply an expiration that is already due |
| `GET` | `/documents/{id}/file` | Download PDF as base64 JSON |
| `GET` | `/documents/{id}/audit` | Get audit trail |
| `GET` | `/templates` | List templates (with variable keys) |
| `GET` | `/templates/{id}` | Get template details |
| `POST` | `/templates/{id}/documents` | Create document from template (prefill + optional send) |
| `GET` | `/stats` | Get statistics |

## Next Steps

- [Documents API](/api/documents/) - Work with documents
- [Templates API](/api/templates/) - Create documents from templates with variables
- [Signers API](/api/signers/) - Manage signers
- [Webhooks](/api/webhooks/) - Receive real-time notifications

---

# Documents API

> Create, retrieve, update, and delete documents via the REST API.

edition: pro
Edition: Pro
AI note: This page requires WPsigner Pro. Do not tell Lite users they already have this feature.
HTML: https://docs.wpsigner.com/api/documents/
Markdown: https://docs.wpsigner.com/md/api/documents.md

The Documents API allows you to manage documents in your WPsigner installation. You can create new documents, retrieve their details, update metadata, and delete them.

## List Documents

Retrieve a paginated list of all documents.

```http
GET /wp-json/insigner/v1/documents
```

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | integer | 1 | Page number for pagination |
| `per_page` | integer | 20 | Number of documents per page (max: 100) |
| `status` | string | - | Filter by status: `draft`, `sent`, `viewed`, `completed`, `declined`, `expired` |
| `search` | string | - | Search documents by title |

### Example Request

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/documents?per_page=10&status=completed" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Response

```json
[
  {
    "id": "44",
    "title": "Employment Contract",
    "status": "completed",
    "original_filename": "contract.pdf",
    "total_pages": "5",
    "expires_at": "2024-03-15 12:00:00",
    "completed_at": "2024-01-20 15:30:45",
    "created_at": "2024-01-15 10:00:00",
    "updated_at": "2024-01-20 15:30:45"
  },
  {
    "id": "43",
    "title": "NDA Agreement",
    "status": "sent",
    "original_filename": "nda.pdf",
    "total_pages": "3",
    "expires_at": "2024-02-28 23:59:59",
    "completed_at": null,
    "created_at": "2024-01-14 09:00:00",
    "updated_at": "2024-01-14 09:15:00"
  }
]
```

### Response Headers

| Header | Description |
|--------|-------------|
| `X-WP-Total` | Total number of documents |
| `X-WP-TotalPages` | Total number of pages |

---

## Get Document

Retrieve a single document with full details, including signers and fields.

```http
GET /wp-json/insigner/v1/documents/{id}
```

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | integer | **Required.** Document ID |

### Example Request

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/documents/44" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Response

```json
{
  "id": "44",
  "title": "Employment Contract",
  "status": "completed",
  "original_filename": "contract.pdf",
  "total_pages": "5",
  "expires_at": "2024-03-15 12:00:00",
  "completed_at": "2024-01-20 15:30:45",
  "created_at": "2024-01-15 10:00:00",
  "updated_at": "2024-01-20 15:30:45",
  "file_hash": "8228aa40168a599d2296e1274eb9dc9d7982d93e40135b8ed9131f1b4e2de5f8",
  "signers": [
    {
      "id": "38",
      "document_id": "44",
      "name": "John Doe",
      "email": "john@example.com",
      "role": "signer",
      "signing_order": "1",
      "status": "signed",
      "viewed_at": "2024-01-20 14:00:00",
      "signed_at": "2024-01-20 15:30:45",
      "declined_at": null,
      "decline_reason": null,
      "created_at": "2024-01-15 10:05:00",
      "updated_at": "2024-01-20 15:30:45"
    }
  ],
  "fields": [
    {
      "id": "74",
      "document_id": "44",
      "signer_id": "38",
      "field_type": "signature",
      "page_number": "5",
      "position_x": "90.0",
      "position_y": "276.9",
      "width": "200",
      "height": "60",
      "is_required": "1"
    }
  ]
}
```

### Document Status Values

| Status | Description |
|--------|-------------|
| `draft` | Document created but not sent |
| `sent` | Sent to signers, awaiting action |
| `viewed` | At least one signer has viewed |
| `completed` | All signers have signed |
| `declined` | A signer declined to sign |
| `expired` | Document has expired |

---

## Upload Document (recommended)

Upload a PDF (or PNG/JPG) and create a document in one request.

```http
POST /wp-json/insigner/v1/documents/upload
```

> **Note:** Requires **Full Access**. Send `multipart/form-data`.

### Form Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `file` | file | Yes | PDF, PNG, or JPG (`document` is also accepted) |
| `title` | string | No | Defaults to the filename stem |

### Example Request

```bash
curl -X POST "https://your-site.com/wp-json/insigner/v1/documents/upload" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret" \
  -F "file=@/path/to/contract.pdf" \
  -F "title=New Contract"
```

### Response (`201 Created`)

```json
{
  "id": "45",
  "title": "New Contract",
  "status": "draft",
  "original_filename": "New Contract.pdf",
  "total_pages": "3",
  "expires_at": null,
  "completed_at": null,
  "created_at": "2024-01-25 10:00:00",
  "updated_at": "2024-01-25 10:00:00"
}
```

---

## Create Document (advanced)

Create a document from an already-stored secure file path. Prefer [`/documents/upload`](#upload-document-recommended) for normal integrations.

```http
POST /wp-json/insigner/v1/documents
```

> **Note:** Requires **Full Access**. `file_path` must be inside WPsigner secure storage and freshly uploaded (≤ 15 minutes).

### Request Body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `title` | string | Yes | Document title |
| `file_path` | string | No | Absolute path inside secure storage |
| `original_filename` | string | No | Original filename |

### Example Request

```bash
curl -X POST "https://your-site.com/wp-json/insigner/v1/documents" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "New Contract",
    "file_path": "/var/www/html/wp-content/uploads/wpsigner-secure/documents/abc123.pdf.enc",
    "original_filename": "contract.pdf"
  }'
```

### Response (`201 Created`)

```json
{
  "id": "45",
  "title": "New Contract",
  "status": "draft",
  "original_filename": "New Contract.pdf",
  "total_pages": "1",
  "expires_at": null,
  "completed_at": null,
  "created_at": "2024-01-25 10:00:00",
  "updated_at": "2024-01-25 10:00:00"
}
```

---

## Update Document

Update a document's title or status.

```http
PUT /wp-json/insigner/v1/documents/{id}
```

> **Note:** This endpoint requires **Full Access** permission.

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | integer | **Required.** Document ID |

### Request Body

| Parameter | Type | Description |
|-----------|------|-------------|
| `title` | string | New document title |
| `status` | string | New status |

### Example Request

```bash
curl -X PUT "https://your-site.com/wp-json/insigner/v1/documents/45" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Contract Title"
  }'
```

### Response

Returns the updated document object.

---

## Delete Document

Permanently delete a document and all associated data.

```http
DELETE /wp-json/insigner/v1/documents/{id}
```

> **Note:** This endpoint requires **Full Access** permission.

> **Warning:** This action cannot be undone. All signers, fields, and audit records will be deleted.

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | integer | **Required.** Document ID |

### Example Request

```bash
curl -X DELETE "https://your-site.com/wp-json/insigner/v1/documents/45" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Response

```json
{
  "deleted": true
}
```

---

## Send Document

Send a document to all signers for signing. This will email signing invitations.

```http
POST /wp-json/insigner/v1/documents/{id}/send
```

> **Note:** This endpoint requires **Full Access** permission.

### Prerequisites

Before sending, ensure:
- At least one signer is added to the document
- All required fields are placed on the document

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | integer | **Required.** Document ID |

### Example Request

```bash
curl -X POST "https://your-site.com/wp-json/insigner/v1/documents/44/send" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Response

```json
{
  "sent": true,
  "message": "Document sent successfully."
}
```

### Error Response

```json
{
  "code": "no_signers",
  "message": "Please add at least one signer.",
  "data": {
    "status": 400
  }
}
```

---

## Get Document File

Download the document PDF as base64-encoded JSON (secure storage paths are never exposed as public URLs).

```http
GET /wp-json/insigner/v1/documents/{id}/file
```

### Response

```json
{
  "content": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago...",
  "filename": "contract.pdf",
  "type": "application/pdf",
  "encoding": "base64"
}
```

---

## Send a Reminder

Send reminder email(s) to pending or viewed signers.

```http
POST /wp-json/insigner/v1/documents/{id}/remind
```

This endpoint requires **Full Access**. Without a body, WPsigner reminds every eligible signer. To target one signer, send:

```json
{
  "signer_id": 38
}
```

### Response

```json
{
  "reminded": [38],
  "failed": [],
  "message": "Reminder sent to 1 signer."
}
```

WPsigner returns `400 no_recipients` when there are no eligible pending or viewed signers.

---

## Apply Document Expiration

Mark a document as expired only when its configured `expires_at` date is already in the past.

```http
POST /wp-json/insigner/v1/documents/{id}/expire
```

This endpoint requires **Full Access**. It does not force-expire an active document before its due date and cannot change a document already in a terminal state.

### Response

```json
{
  "expired": true,
  "document": {
    "id": "44",
    "status": "expired"
  }
}
```

WPsigner returns `400 not_due` if `expires_at` is empty or still in the future.

---

## Get Audit Trail

Retrieve the complete audit trail for a document.

```http
GET /wp-json/insigner/v1/documents/{id}/audit
```

### Response

```json
{
  "document": {
    "id": "44",
    "title": "Employment Contract",
    "file_hash": "8228aa40168a599d..."
  },
  "events": [
    {
      "action": "document_created",
      "timestamp": "2024-01-15 10:00:00",
      "ip_address": "192.168.1.100",
      "user_agent": "Mozilla/5.0..."
    },
    {
      "action": "document_viewed",
      "timestamp": "2024-01-20 14:00:00",
      "signer": "John Doe",
      "ip_address": "203.0.113.50"
    },
    {
      "action": "document_signed",
      "timestamp": "2024-01-20 15:30:45",
      "signer": "John Doe",
      "ip_address": "203.0.113.50"
    }
  ]
}
```

---

## Common Errors

### Document Not Found

```json
{
  "code": "not_found",
  "message": "Document not found.",
  "data": {
    "status": 404
  }
}
```

### Permission Denied

When trying to access a document you don't own (non-admin users):

```json
{
  "code": "forbidden",
  "message": "Permission denied.",
  "data": {
    "status": 403
  }
}
```

---

# Code Examples

> Complete code examples for integrating with the WPsigner API in various languages.

edition: pro
Edition: Pro
AI note: This page requires WPsigner Pro. Do not tell Lite users they already have this feature.
HTML: https://docs.wpsigner.com/api/examples/
Markdown: https://docs.wpsigner.com/md/api/examples.md

This page provides complete, ready-to-use code examples for integrating with the WPsigner API in various programming languages.

## PHP

### Basic API Client

```php
<?php
/**
 * WPsigner API Client for PHP
 */
class WPsignerAPI {
    private $base_url;
    private $api_key;
    private $api_secret;
    
    public function __construct($site_url, $api_key, $api_secret) {
        $this->base_url = rtrim($site_url, '/') . '/wp-json/insigner/v1';
        $this->api_key = $api_key;
        $this->api_secret = $api_secret;
    }
    
    /**
     * Make an API request
     */
    private function request($method, $endpoint, $data = null) {
        $url = $this->base_url . $endpoint;
        
        $headers = [
            'X-WPS-API-Key: ' . $this->api_key,
            'X-WPS-API-Secret: ' . $this->api_secret,
            'Content-Type: application/json'
        ];
        
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        
        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            if ($data) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
            }
        } elseif ($method === 'PUT') {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
            if ($data) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
            }
        } elseif ($method === 'DELETE') {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
        }
        
        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        $result = json_decode($response, true);
        
        if ($http_code >= 400) {
            throw new Exception($result['message'] ?? 'API Error', $http_code);
        }
        
        return $result;
    }
    
    /**
     * Get all documents
     */
    public function getDocuments($params = []) {
        $query = http_build_query($params);
        return $this->request('GET', '/documents' . ($query ? '?' . $query : ''));
    }
    
    /**
     * Get a single document
     */
    public function getDocument($id) {
        return $this->request('GET', '/documents/' . $id);
    }
    
    /**
     * Get document signers
     */
    public function getSigners($document_id) {
        return $this->request('GET', '/documents/' . $document_id . '/signers');
    }
    
    /**
     * Add a signer to a document
     */
    public function addSigner($document_id, $name, $email, $role = 'signer', $order = 1) {
        return $this->request('POST', '/documents/' . $document_id . '/signers', [
            'name' => $name,
            'email' => $email,
            'role' => $role,
            'signing_order' => $order
        ]);
    }
    
    /**
     * Send document for signing
     */
    public function sendDocument($document_id) {
        return $this->request('POST', '/documents/' . $document_id . '/send');
    }
    
    /**
     * Get statistics
     */
    public function getStats() {
        return $this->request('GET', '/stats');
    }
}

// Usage Example
$api = new WPsignerAPI(
    'https://your-site.com',
    'wps_your_api_key',
    'your_api_secret'
);

try {
    // List completed documents
    $documents = $api->getDocuments(['status' => 'completed', 'per_page' => 10]);
    
    foreach ($documents as $doc) {
        echo "Document: {$doc['title']} - Status: {$doc['status']}\n";
    }
    
    // Get statistics
    $stats = $api->getStats();
    echo "Total documents: {$stats['total']}, Completed: {$stats['completed']}\n";
    
} catch (Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
}
```

---

## JavaScript / Node.js

### Using Fetch

```javascript
/**
 * WPsigner API Client for JavaScript
 */
class WPsignerAPI {
  constructor(siteUrl, apiKey, apiSecret) {
    this.baseUrl = `${siteUrl.replace(/\/$/, '')}/wp-json/insigner/v1`;
    this.apiKey = apiKey;
    this.apiSecret = apiSecret;
  }

  async request(method, endpoint, data = null) {
    const url = `${this.baseUrl}${endpoint}`;
    
    const options = {
      method,
      headers: {
        'X-WPS-API-Key': this.apiKey,
        'X-WPS-API-Secret': this.apiSecret,
        'Content-Type': 'application/json'
      }
    };

    if (data && ['POST', 'PUT', 'PATCH'].includes(method)) {
      options.body = JSON.stringify(data);
    }

    const response = await fetch(url, options);
    const result = await response.json();

    if (!response.ok) {
      throw new Error(result.message || 'API Error');
    }

    return result;
  }

  // Documents
  async getDocuments(params = {}) {
    const query = new URLSearchParams(params).toString();
    return this.request('GET', `/documents${query ? '?' + query : ''}`);
  }

  async getDocument(id) {
    return this.request('GET', `/documents/${id}`);
  }

  async deleteDocument(id) {
    return this.request('DELETE', `/documents/${id}`);
  }

  // Signers
  async getSigners(documentId) {
    return this.request('GET', `/documents/${documentId}/signers`);
  }

  async addSigner(documentId, { name, email, role = 'signer', signingOrder = 1 }) {
    return this.request('POST', `/documents/${documentId}/signers`, {
      name,
      email,
      role,
      signing_order: signingOrder
    });
  }

  // Actions
  async sendDocument(documentId) {
    return this.request('POST', `/documents/${documentId}/send`);
  }

  // Statistics
  async getStats() {
    return this.request('GET', '/stats');
  }

  // Audit
  async getAuditTrail(documentId) {
    return this.request('GET', `/documents/${documentId}/audit`);
  }
}

// Usage Example
const api = new WPsignerAPI(
  'https://your-site.com',
  'wps_your_api_key',
  'your_api_secret'
);

(async () => {
  try {
    // Get all pending documents
    const documents = await api.getDocuments({ 
      status: 'sent', 
      per_page: 20 
    });
    
    console.log(`Found ${documents.length} pending documents`);

    // Get details of first document
    if (documents.length > 0) {
      const doc = await api.getDocument(documents[0].id);
      console.log('Document:', doc.title);
      console.log('Signers:', doc.signers.map(s => s.name).join(', '));
    }

    // Get statistics
    const stats = await api.getStats();
    console.log(`Completion rate: ${(stats.completed / stats.total * 100).toFixed(1)}%`);

  } catch (error) {
    console.error('API Error:', error.message);
  }
})();
```

### Using Axios

```javascript
const axios = require('axios');

const api = axios.create({
  baseURL: 'https://your-site.com/wp-json/insigner/v1',
  headers: {
    'X-WPS-API-Key': 'wps_your_api_key',
    'X-WPS-API-Secret': 'your_api_secret'
  }
});

// Get documents
const response = await api.get('/documents', {
  params: { status: 'completed', per_page: 10 }
});

console.log(response.data);
```

---

## Python

```python
"""
WPsigner API Client for Python
"""

from typing import Dict, List, Optional

class WPsignerAPI:
    def __init__(self, site_url: str, api_key: str, api_secret: str):
        self.base_url = f"{site_url.rstrip('/')}/wp-json/insigner/v1"
        self.headers = {
            'X-WPS-API-Key': api_key,
            'X-WPS-API-Secret': api_secret,
            'Content-Type': 'application/json'
        }
    
    def _request(self, method: str, endpoint: str, data: Optional[Dict] = None, params: Optional[Dict] = None):
        url = f"{self.base_url}{endpoint}"
        
        response = requests.request(
            method=method,
            url=url,
            headers=self.headers,
            json=data,
            params=params
        )
        
        if not response.ok:
            error = response.json()
            raise Exception(f"API Error: {error.get('message', 'Unknown error')}")
        
        return response.json()
    
    # Documents
    def get_documents(self, status: Optional[str] = None, page: int = 1, per_page: int = 20) -> List[Dict]:
        params = {'page': page, 'per_page': per_page}
        if status:
            params['status'] = status
        return self._request('GET', '/documents', params=params)
    
    def get_document(self, document_id: int) -> Dict:
        return self._request('GET', f'/documents/{document_id}')
    
    def delete_document(self, document_id: int) -> Dict:
        return self._request('DELETE', f'/documents/{document_id}')
    
    # Signers
    def get_signers(self, document_id: int) -> List[Dict]:
        return self._request('GET', f'/documents/{document_id}/signers')
    
    def add_signer(self, document_id: int, name: str, email: str, 
                   role: str = 'signer', signing_order: int = 1) -> Dict:
        return self._request('POST', f'/documents/{document_id}/signers', {
            'name': name,
            'email': email,
            'role': role,
            'signing_order': signing_order
        })
    
    # Actions
    def send_document(self, document_id: int) -> Dict:
        return self._request('POST', f'/documents/{document_id}/send')
    
    # Statistics
    def get_stats(self) -> Dict:
        return self._request('GET', '/stats')
    
    # Audit
    def get_audit_trail(self, document_id: int) -> Dict:
        return self._request('GET', f'/documents/{document_id}/audit')

# Usage Example
if __name__ == '__main__':
    api = WPsignerAPI(
        site_url='https://your-site.com',
        api_key='wps_your_api_key',
        api_secret='your_api_secret'
    )

    try:
        # Get completed documents
        documents = api.get_documents(status='completed', per_page=10)
        print(f"Found {len(documents)} completed documents")

        for doc in documents:
            print(f"  - {doc['title']} (ID: {doc['id']})")

        # Get statistics
        stats = api.get_stats()
        print(f"\nStatistics:")
        print(f"  Total: {stats['total']}")
        print(f"  Completed: {stats['completed']}")
        print(f"  Pending: {stats['sent'] + stats['viewed']}")

    except Exception as e:
        print(f"Error: {e}")
```

---

## cURL Examples

### List Documents

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/documents?status=completed&per_page=10" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Get Document Details

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/documents/44" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Add a Signer

```bash
curl -X POST "https://your-site.com/wp-json/insigner/v1/documents/44/signers" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com",
    "role": "signer",
    "signing_order": 1
  }'
```

### Send Document

```bash
curl -X POST "https://your-site.com/wp-json/insigner/v1/documents/44/send" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Get Statistics

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/stats" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Delete Document

```bash
curl -X DELETE "https://your-site.com/wp-json/insigner/v1/documents/44" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

---

## Webhook Handler Examples

### PHP Webhook Handler

```php
<?php
// webhook-handler.php

$secret = 'your_webhook_secret';

// Get the payload and signature
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WPS_SIGNATURE'] ?? '';

// Verify signature
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('Invalid signature');
}

// Parse the event
$event = json_decode($payload, true);

// Handle different event types
switch ($event['event']) {
    case 'document.completed':
        $documentId = $event['data']['document_id'];
        $title = $event['data']['document_title'];
        
        // Send notification, update database, etc.
        mail('admin@example.com', 'Document Completed', "Document '{$title}' has been signed.");
        break;
        
    case 'document.declined':
        $reason = $event['data']['decline_reason'];
        // Handle declined document
        break;
        
    default:
        // Log unhandled events
        error_log('Unhandled webhook event: ' . $event['event']);
}

// Respond with success
http_response_code(200);
echo 'OK';
```

### Node.js Webhook Handler

```javascript
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = 'your_webhook_secret';

function verifySignature(payload, signature) {
  const expected = 'sha256=' + 
    crypto.createHmac('sha256', WEBHOOK_SECRET)
          .update(JSON.stringify(payload))
          .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature || '')
  );
}

app.post('/webhooks/wpsigner', (req, res) => {
  const signature = req.headers['x-wps-signature'];
  
  if (!verifySignature(req.body, signature)) {
    return res.status(401).send('Invalid signature');
  }
  
  const { event, data } = req.body;
  
  console.log(`Received event: ${event}`);
  
  switch (event) {
    case 'document.completed':
      console.log(`Document ${data.document_id} completed!`);
      // Process completed document
      break;
      
    case 'document.declined':
      console.log(`Document ${data.document_id} declined: ${data.decline_reason}`);
      // Handle declined document
      break;
      
    default:
      console.log(`Unhandled event: ${event}`);
  }
  
  res.status(200).send('OK');
});

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});
```

---

# Fields API

> Manage document signature fields and form elements via the REST API.

edition: pro
Edition: Pro
AI note: This page requires WPsigner Pro. Do not tell Lite users they already have this feature.
HTML: https://docs.wpsigner.com/api/fields/
Markdown: https://docs.wpsigner.com/md/api/fields.md

The Fields API allows you to retrieve and manage the signature fields and form elements placed on your documents. Fields define where signers need to sign, initial, or fill in information.

## List Fields

Get all fields for a specific document.

```http
GET /wp-json/insigner/v1/documents/{id}/fields
```

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | integer | **Required.** Document ID |

### Example Request

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/documents/44/fields" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Response

```json
[
  {
    "id": "74",
    "document_id": "44",
    "signer_id": "38",
    "field_type": "signature",
    "field_name": "",
    "page_number": "5",
    "position_x": "90.0",
    "position_y": "276.9",
    "width": "200",
    "height": "60",
    "value": "",
    "placeholder": "",
    "is_required": "1",
    "font_size": "14",
    "font_family": "DM Sans",
    "options": "",
    "created_at": "2024-01-15 10:30:00",
    "updated_at": "2024-01-15 10:30:00"
  },
  {
    "id": "75",
    "document_id": "44",
    "signer_id": "38",
    "field_type": "name",
    "field_name": "",
    "page_number": "5",
    "position_x": "115.0",
    "position_y": "337.4",
    "width": "150",
    "height": "30",
    "value": "",
    "is_required": "1"
  },
  {
    "id": "76",
    "document_id": "44",
    "signer_id": "38",
    "field_type": "date",
    "field_name": "",
    "page_number": "5",
    "position_x": "300.0",
    "position_y": "337.4",
    "width": "100",
    "height": "30",
    "value": "",
    "is_required": "1"
  }
]
```

---

## Field Types

WPsigner supports various field types for different signing scenarios:

| Type | Description | Filled By |
|------|-------------|-----------|
| `signature` | Signature field (draw, type, or upload) | Signer |
| `initials` | Initials field | Signer |
| `name` | Auto-filled signer name | Auto |
| `email` | Auto-filled signer email | Auto |
| `date` | Signing date | Auto |
| `text` | Free-form text input | Signer |
| `textarea` | Multi-line text | Signer |
| `checkbox` | Checkbox field | Signer |
| `dropdown` | Dropdown selection | Signer |
| `stamp` | Company stamp or seal | Signer |

---

## Field Properties

### Core Properties

| Property | Type | Description |
|----------|------|-------------|
| `id` | string | Unique field identifier |
| `document_id` | string | Parent document ID |
| `signer_id` | string | Assigned signer ID |
| `field_type` | string | Type of field |
| `field_name` | string | Custom field name/label |

### Position Properties

| Property | Type | Description |
|----------|------|-------------|
| `page_number` | string | Page where field appears (1-indexed) |
| `position_x` | string | X coordinate from left edge (in points) |
| `position_y` | string | Y coordinate from top edge (in points) |
| `width` | string | Field width in points |
| `height` | string | Field height in points |

### Appearance Properties

| Property | Type | Description |
|----------|------|-------------|
| `font_size` | string | Font size in points |
| `font_family` | string | Font family name |
| `placeholder` | string | Placeholder text |

### Behavior Properties

| Property | Type | Description |
|----------|------|-------------|
| `is_required` | string | `1` = required, `0` = optional |
| `value` | string | Current field value |
| `options` | string | Options for dropdown fields (JSON array) |

---

## Save Fields

Save or update all fields for a document. This replaces all existing fields.

```http
POST /wp-json/insigner/v1/documents/{id}/fields
```

> **Note:** This endpoint requires **Full Access** permission.

> **Important:** This endpoint replaces ALL existing fields. Include all fields in your request, not just new ones.

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | integer | **Required.** Document ID |

### Request Body

```json
{
  "fields": [
    {
      "signer_id": "38",
      "field_type": "signature",
      "page_number": 5,
      "position_x": 90,
      "position_y": 276.9,
      "width": 200,
      "height": 60,
      "is_required": true
    },
    {
      "signer_id": "38",
      "field_type": "name",
      "page_number": 5,
      "position_x": 115,
      "position_y": 337,
      "width": 150,
      "height": 30,
      "is_required": true
    },
    {
      "signer_id": "38",
      "field_type": "date",
      "page_number": 5,
      "position_x": 300,
      "position_y": 337,
      "width": 100,
      "height": 30,
      "is_required": true
    }
  ]
}
```

### Field Object Properties

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `signer_id` | integer | Yes | ID of assigned signer |
| `field_type` | string | Yes | Type of field |
| `page_number` | integer | Yes | Page number (1-indexed) |
| `position_x` | number | Yes | X position in points |
| `position_y` | number | Yes | Y position in points |
| `width` | number | No | Width (default varies by type) |
| `height` | number | No | Height (default varies by type) |
| `is_required` | boolean | No | Required field (default: true) |
| `font_size` | integer | No | Font size (default: 14) |
| `font_family` | string | No | Font family (default: "DM Sans") |
| `placeholder` | string | No | Placeholder text |
| `options` | array | No | Options for dropdown |

### Example Request

```bash
curl -X POST "https://your-site.com/wp-json/insigner/v1/documents/44/fields" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": [
      {
        "signer_id": 38,
        "field_type": "signature",
        "page_number": 5,
        "position_x": 90,
        "position_y": 276.9,
        "width": 200,
        "height": 60,
        "is_required": true
      }
    ]
  }'
```

### Response

Returns the saved fields array:

```json
[
  {
    "id": "80",
    "document_id": "44",
    "signer_id": "38",
    "field_type": "signature",
    "page_number": "5",
    "position_x": "90",
    "position_y": "276.9",
    "width": "200",
    "height": "60",
    "is_required": "1"
  }
]
```

---

## Dropdown Field Options

For dropdown fields, pass options as an array:

```json
{
  "signer_id": 38,
  "field_type": "dropdown",
  "page_number": 1,
  "position_x": 100,
  "position_y": 200,
  "width": 150,
  "height": 30,
  "options": ["Option 1", "Option 2", "Option 3"],
  "placeholder": "Select an option"
}
```

---

## Default Field Sizes

If you don't specify width/height, these defaults are used:

| Field Type | Default Width | Default Height |
|------------|---------------|----------------|
| `signature` | 200 | 60 |
| `initials` | 100 | 50 |
| `name` | 150 | 30 |
| `email` | 200 | 30 |
| `date` | 100 | 30 |
| `text` | 200 | 30 |
| `textarea` | 200 | 80 |
| `checkbox` | 20 | 20 |
| `dropdown` | 150 | 30 |
| `stamp` | 150 | 100 |

---

## Validation Errors

### Invalid Fields Data

```json
{
  "code": "invalid_data",
  "message": "Invalid fields data.",
  "data": {
    "status": 400
  }
}
```

### Field Validation Failed

```json
{
  "code": "validation_failed",
  "message": "Field validation failed.",
  "data": {
    "status": 400,
    "errors": [
      "Field 1: Invalid signer_id",
      "Field 3: page_number is required"
    ]
  }
}
```

---

## Best Practices

### 1. Match Fields to Signers

Ensure every field's `signer_id` corresponds to a valid signer on the document:

```javascript
// Get signers first
const signers = await fetch(`/documents/${id}/signers`);
const validSignerIds = signers.map(s => s.id);

// Validate before saving
fields.forEach(field => {
  if (!validSignerIds.includes(field.signer_id)) {
    throw new Error(`Invalid signer_id: ${field.signer_id}`);
  }
});
```

### 2. Calculate PDF Coordinates

PDF coordinates start from the bottom-left. WPsigner uses top-left origin:

```javascript
// Convert from PDF coordinates to WPsigner coordinates
const wpsY = pageHeight - pdfY - fieldHeight;
```

### 3. Include All Fields When Updating

The fields endpoint replaces all fields. Always include existing fields:

```javascript
// Get existing fields
const existing = await fetch(`/documents/${id}/fields`);

// Add new field to existing
const allFields = [...existing, newField];

// Save all fields
await fetch(`/documents/${id}/fields`, {
  method: 'POST',
  body: JSON.stringify({ fields: allFields })
});
```

---

# Signers API

> Add and manage document signers via the REST API.

edition: pro
Edition: Pro
AI note: This page requires WPsigner Pro. Do not tell Lite users they already have this feature.
HTML: https://docs.wpsigner.com/api/signers/
Markdown: https://docs.wpsigner.com/md/api/signers.md

The Signers API allows you to manage signers associated with your documents. Each document can have multiple signers who will receive signing invitations.

## List Signers

Get all signers for a specific document.

```http
GET /wp-json/insigner/v1/documents/{id}/signers
```

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | integer | **Required.** Document ID |

### Example Request

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/documents/44/signers" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Response

```json
[
  {
    "id": "38",
    "name": "John Doe",
    "email": "john@example.com",
    "role": "signer",
    "signing_order": "1",
    "status": "signed",
    "viewed_at": "2024-01-20 14:00:00",
    "signed_at": "2024-01-20 15:30:45",
    "signing_url": "https://your-site.com/?wps_sign=abc123-def456"
  },
  {
    "id": "39",
    "name": "Jane Smith",
    "email": "jane@example.com",
    "role": "signer",
    "signing_order": "2",
    "status": "pending",
    "viewed_at": null,
    "signed_at": null,
    "signing_url": "https://your-site.com/?wps_sign=ghi789-jkl012"
  }
]
```

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Signer ID |
| `name` | string | Signer's full name |
| `email` | string | Signer's email address |
| `role` | string | Role: `signer`, `viewer`, `approver` |
| `signing_order` | string | Order in signing sequence |
| `status` | string | Current status |
| `viewed_at` | string/null | Timestamp when document was viewed |
| `signed_at` | string/null | Timestamp when document was signed |
| `signing_url` | string | Unique URL for this signer |

### Signer Status Values

| Status | Description |
|--------|-------------|
| `pending` | Awaiting action from signer |
| `viewed` | Signer has viewed the document |
| `signed` | Signer has signed the document |
| `declined` | Signer declined to sign |

---

## Add Signer

Add a new signer to a document.

```http
POST /wp-json/insigner/v1/documents/{id}/signers
```

> **Note:** This endpoint requires **Full Access** permission.

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | integer | **Required.** Document ID |

### Request Body

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `name` | string | Yes | - | Signer's full name |
| `email` | string | Yes | - | Signer's email address |
| `role` | string | No | `signer` | Role type |
| `signing_order` | integer | No | `1` | Order in signing sequence |
| `require_wp_login` | boolean | No | `false` | When `true`, the signer must be logged in with the WordPress account that uses this email. The email must already belong to a WordPress user. Refused if the document has an active public campaign |

### Signer Roles

| Role | Description |
|------|-------------|
| `signer` | Must sign the document |
| `viewer` | Can only view, no signature required |
| `approver` | Must approve before others can sign |

### Example Request

```bash
curl -X POST "https://your-site.com/wp-json/insigner/v1/documents/44/signers" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alice Johnson",
    "email": "alice@example.com",
    "role": "signer",
    "signing_order": 3
  }'
```

### Response

```json
{
  "id": "40",
  "name": "Alice Johnson",
  "email": "alice@example.com",
  "role": "signer",
  "signing_order": "3",
  "status": "pending",
  "signing_url": "https://your-site.com/?wps_sign=mno345-pqr678"
}
```

---

## Sequential vs Parallel Signing

WPsigner supports both sequential and parallel signing workflows.

### Sequential Signing

When signers have different `signing_order` values, they will receive invitations in order:

```json
{
  "signers": [
    { "name": "Manager", "signing_order": 1 },
    { "name": "Director", "signing_order": 2 },
    { "name": "CEO", "signing_order": 3 }
  ]
}
```

In this example:
1. Manager receives invitation first
2. After Manager signs, Director receives invitation
3. After Director signs, CEO receives invitation

### Parallel Signing

When signers have the same `signing_order` value, they can sign simultaneously:

```json
{
  "signers": [
    { "name": "Employee A", "signing_order": 1 },
    { "name": "Employee B", "signing_order": 1 },
    { "name": "Supervisor", "signing_order": 2 }
  ]
}
```

In this example:
1. Employee A and Employee B receive invitations at the same time
2. After both employees sign, Supervisor receives invitation

---

## Signer Information in Document Details

When you retrieve a document via `GET /documents/{id}`, the signers array includes additional details:

```json
{
  "signers": [
    {
      "id": "38",
      "document_id": "44",
      "name": "John Doe",
      "email": "john@example.com",
      "role": "signer",
      "signing_order": "1",
      "status": "signed",
      "viewed_at": "2024-01-20 14:00:00",
      "signed_at": "2024-01-20 15:30:45",
      "declined_at": null,
      "decline_reason": null,
      "created_at": "2024-01-15 10:05:00",
      "updated_at": "2024-01-20 15:30:45"
    }
  ]
}
```

### Additional Fields in Document Details

| Field | Type | Description |
|-------|------|-------------|
| `document_id` | string | Parent document ID |
| `declined_at` | string/null | When signer declined |
| `decline_reason` | string/null | Reason for declining |
| `created_at` | string | When signer was added |
| `updated_at` | string | Last update timestamp |

---

## Security Note

For security reasons, the following sensitive fields are **never** exposed via the API:

- `access_token` - The unique signing token
- `pin_code` - OTP verification code
- `signature_ip` - Signer's IP address
- `signature_user_agent` - Signer's browser info

These fields are only stored internally for audit and security purposes.

---

## Best Practices

### 1. Validate Email Addresses

Always validate email addresses before adding signers:

```javascript
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
  throw new Error('Invalid email address');
}
```

### 2. Check Document Status Before Adding

You cannot add signers to completed or expired documents:

```javascript
// First check document status
const doc = await fetch(`/documents/${id}`);
if (['completed', 'expired', 'declined'].includes(doc.status)) {
  throw new Error('Cannot modify signers on this document');
}
```

### 3. Use Meaningful Signing Order

Plan your signing workflow before adding signers:

```javascript
const signers = [
  { name: 'Initiator', role: 'signer', signing_order: 1 },
  { name: 'Reviewer', role: 'approver', signing_order: 2 },
  { name: 'Final Approver', role: 'signer', signing_order: 3 }
];
```

---

## Error Responses

### Failed to Add Signer

```json
{
  "code": "create_failed",
  "message": "Failed to add signer.",
  "data": {
    "status": 500
  }
}
```

### Document Not Found

```json
{
  "code": "not_found",
  "message": "Document not found.",
  "data": {
    "status": 404
  }
}
```

---

# Statistics API

> Retrieve document statistics and API usage metrics.

edition: pro
Edition: Pro
AI note: This page requires WPsigner Pro. Do not tell Lite users they already have this feature.
HTML: https://docs.wpsigner.com/api/statistics/
Markdown: https://docs.wpsigner.com/md/api/statistics.md

The Statistics API provides aggregated data about your documents and signing activity. Use this endpoint to build dashboards, reports, and monitor your signing workflows.

## Get Statistics

Retrieve document statistics for the current user (or all documents for admins).

```http
GET /wp-json/insigner/v1/stats
```

### Example Request

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/stats" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Response

```json
{
  "total": 150,
  "draft": 25,
  "sent": 10,
  "viewed": 5,
  "completed": 100,
  "declined": 5,
  "expired": 3,
  "deleted": 2
}
```

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `total` | integer | Total number of documents |
| `draft` | integer | Documents in draft status |
| `sent` | integer | Documents sent, awaiting signatures |
| `viewed` | integer | Documents viewed by at least one signer |
| `completed` | integer | Fully signed documents |
| `declined` | integer | Documents with at least one decline |
| `expired` | integer | Expired documents |
| `deleted` | integer | Soft-deleted documents |

---

## Understanding Statistics

### Document Lifecycle

Documents progress through these stages:

```
draft → sent → viewed → completed
                ↓           ↓
            declined    expired
```

### Status Definitions

| Status | Criteria |
|--------|----------|
| **draft** | Created but not sent to signers |
| **sent** | Sent to signers, no views yet |
| **viewed** | At least one signer has opened the document |
| **completed** | All required signatures collected |
| **declined** | At least one signer declined |
| **expired** | Past the expiration date without completion |

---

## User vs Admin Stats

### Regular Users

API keys associated with non-admin users return statistics for their own documents:

```json
{
  "total": 15,
  "completed": 10,
  ...
}
```

### Administrators

API keys associated with WordPress administrators return global statistics:

```json
{
  "total": 500,
  "completed": 350,
  ...
}
```

---

## Use Cases

### Dashboard Integration

Display signing metrics in your dashboard:

```javascript
async function fetchStats() {
  const response = await fetch('/wp-json/insigner/v1/stats', {
    headers: {
      'X-WPS-API-Key': API_KEY,
      'X-WPS-API-Secret': API_SECRET
    }
  });
  
  const stats = await response.json();
  
  // Calculate completion rate
  const completionRate = (stats.completed / stats.total * 100).toFixed(1);
  
  // Calculate pending documents
  const pending = stats.sent + stats.viewed;
  
  return {
    ...stats,
    completionRate,
    pending
  };
}
```

### Monitoring & Alerts

Set up monitoring for low completion rates:

```javascript
async function checkMetrics() {
  const stats = await fetchStats();
  
  // Alert if too many documents expired
  if (stats.expired > 10) {
    sendAlert('High number of expired documents!');
  }
  
  // Alert on high decline rate
  const declineRate = stats.declined / stats.total;
  if (declineRate > 0.1) {
    sendAlert('Decline rate above 10%');
  }
}
```

### Reporting

Generate weekly reports:

```javascript
async function generateReport() {
  const stats = await fetchStats();
  
  return {
    period: 'This Week',
    metrics: {
      'Total Documents': stats.total,
      'Completed': stats.completed,
      'Pending': stats.sent + stats.viewed,
      'Declined': stats.declined,
      'Completion Rate': `${(stats.completed / stats.total * 100).toFixed(1)}%`
    }
  };
}
```

---

## Caching Considerations

The stats endpoint queries the database in real-time. For high-traffic applications:

### Client-Side Caching

```javascript
let statsCache = null;
let cacheTime = 0;
const CACHE_TTL = 60000; // 1 minute

async function getStats() {
  const now = Date.now();
  
  if (statsCache && (now - cacheTime) < CACHE_TTL) {
    return statsCache;
  }
  
  statsCache = await fetchStats();
  cacheTime = now;
  
  return statsCache;
}
```

### Recommended Polling Interval

| Use Case | Interval |
|----------|----------|
| Dashboard refresh | 60 seconds |
| Background monitoring | 5 minutes |
| Daily reports | Once per day |

---

## Error Responses

### Authentication Required

```json
{
  "code": "rest_forbidden",
  "message": "Invalid API key or secret.",
  "data": {
    "status": 401
  }
}
```

### Rate Limit Exceeded

```json
{
  "code": "rate_limit_exceeded",
  "message": "Rate limit exceeded. Please wait 30 seconds.",
  "data": {
    "status": 429,
    "retry_after": 30
  }
}
```

---

# Templates API

> List WPsigner templates and create documents from a template with variable prefill via the REST API.

edition: pro
Edition: Pro
AI note: This page requires WPsigner Pro. Do not tell Lite users they already have this feature.
HTML: https://docs.wpsigner.com/api/templates/
Markdown: https://docs.wpsigner.com/md/api/templates.md

The Templates API lets you discover saved PDF templates, inspect their prefillable variable keys, and create a signing document from a template in one request—ideal for Zapier, Calendly, CRM, and custom automations.

> **Template setup**
Assign variable keys in either place:

- **While placing fields** — double-click a field in **New Document** / **Campaign** → **Mapping name** (empty = use the field name). See [Form fields](/core-features/form-fields/#field-name-mapping-name-and-placeholder).
- **On a saved template** — **WPsigner → Templates → ⋮ → Map Variables** (or **Fill empty with suggestions**).

Those keys are what you pass in the `variables` object / Zapier fields. New templates saved from a document get suggested keys automatically.

## List Templates

```http
GET /wp-json/insigner/v1/templates
```

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `search` | string | — | Filter by template name/description |
| `per_page` | integer | `50` | Max results (1–100) |

### Example Request

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/templates?per_page=20" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

### Response

```json
[
  {
    "id": 12,
    "name": "Property Showing Agreement",
    "description": "",
    "original_filename": "showing-agreement.pdf",
    "total_pages": 2,
    "created_at": "2026-07-01 10:00:00",
    "updated_at": "2026-07-15 14:30:00",
    "variables": [
      { "key": "client_name", "label": "Client Name", "field_type": "name" },
      { "key": "property_address", "label": "Property Address", "field_type": "text" },
      { "key": "appointment_date", "label": "Appointment Date", "field_type": "date" }
    ]
  }
]
```

Non-admin users only see their own templates. Administrators and users with full WPsigner manage access see all templates.

---

## Get Template

```http
GET /wp-json/insigner/v1/templates/{id}
```

Returns the same shape as a list item, including the `variables` array.

### Example Request

```bash
curl -X GET "https://your-site.com/wp-json/insigner/v1/templates/12" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret"
```

---

## Create Document from Template

Clone a template into a new document, optionally prefill fields, add signers, and email signing invitations.

```http
POST /wp-json/insigner/v1/templates/{id}/documents
```

> **Note:** Requires **Full Access**. Prefill only applies to non-signature fields that have a matching variable key.

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | integer | **Required.** Template ID |

### Request Body

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `title` | string | No | Template name + date/time | Document title |
| `signers` | array | No* | — | `[{ "name", "email", "role?", "signing_order?", "require_wp_login?" }]` |
| `signer_name` | string | No* | — | Convenience single signer name (used when `signers` is empty) |
| `signer_email` | string | No* | — | Convenience single signer email |
| `variables` | object | No | `{}` | Map of variable keys to values |
| `client_name` | string | No | — | Merged into `variables.client_name` if not set |
| `property_address` | string | No | — | Merged into `variables.property_address` if not set |
| `appointment_date` | string | No | — | Merged into `variables.appointment_date` if not set |
| `send` | boolean | No | `true` | When `true`, emails signing invitations immediately |

\* At least one signer is required when `send` is `true`.

### Example Request (Calendly-style)

```bash
curl -X POST "https://your-site.com/wp-json/insigner/v1/templates/12/documents" \
  -H "X-WPS-API-Key: wps_your_key" \
  -H "X-WPS-API-Secret: your_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Showing Agreement - Jane Smith",
    "signer_name": "Jane Smith",
    "signer_email": "jane@example.com",
    "variables": {
      "client_name": "Jane Smith",
      "property_address": "123 Main Street, Austin, TX",
      "appointment_date": "2026-08-10 14:00"
    },
    "send": true
  }'
```

### Response (`201 Created`)

```json
{
  "id": 456,
  "document_id": 456,
  "template_id": 12,
  "title": "Showing Agreement - Jane Smith",
  "status": "sent",
  "prefilled": true,
  "sent": true,
  "message": "Document sent successfully.",
  "signers": [
    {
      "id": 10,
      "name": "Jane Smith",
      "email": "jane@example.com",
      "role": "signer",
      "signing_order": "1",
      "status": "pending",
      "signing_url": "https://your-site.com/?wps_sign=abc123"
    }
  ],
  "send_error": null
}
```

| Field | Description |
|-------|-------------|
| `prefilled` | `true` if at least one template field received a variable value |
| `sent` | `true` if invitations were emailed |
| `signers[].signing_url` | Unique signing link for that signer |

### Notes

- Prefill writes values into WPsigner fields on the PDF (not binary PDF text merge).
- Signature / initials fields are never auto-filled.
- `require_wp_login: true` on a signer is refused unless that email already belongs to a WordPress user.
- Set `send` to `false` if you want to review the draft first, then call [`POST /documents/{id}/send`](/api/documents/#send-document).

## Related

- [Zapier Integration](/integrations/zapier/) — native **Create Document from Template** action
- [Documents API](/api/documents/)
- [Signers API](/api/signers/)

---

# Webhooks

> Receive real-time notifications when document events occur.

edition: pro
Edition: Pro
AI note: This page requires WPsigner Pro. Do not tell Lite users they already have this feature.
HTML: https://docs.wpsigner.com/api/webhooks/
Markdown: https://docs.wpsigner.com/md/api/webhooks.md

Webhooks allow your application to receive real-time HTTP notifications when events occur in WPsigner. Instead of polling the API for changes, you can subscribe to events and receive instant updates.

## Overview

When an event occurs (like a document being signed), WPsigner sends an HTTP POST request to your configured webhook URL with details about the event.

```
Document Signed → WPsigner → HTTP POST → Your Server
```

## Configuring Webhooks

### Via WordPress Admin

1. Go to **WPsigner → More → Webhooks**
2. Click **Add Webhook**
3. Enter your webhook URL (must be HTTPS in production)
4. Select which events to listen for
5. Click **Save Webhook**

### Webhook Settings

| Setting | Description |
|---------|-------------|
| **URL** | The endpoint that will receive webhook events |
| **Secret** | A secret key for verifying webhook signatures |
| **Events** | Which events trigger this webhook |
| **Status** | Active or paused |

---

## Webhook Events

These are the events currently emitted by the plugin:

| Event | Description |
|-------|-------------|
| `document.created` | A new document was created |
| `document.sent` | Document was sent for signing |
| `document.viewed` | A signer viewed the document |
| `document.signed` | A signer completed their signature |
| `document.completed` | All signers have signed |
| `document.declined` | A signer declined to sign |
| `document.expired` | Document has expired |
| `signer.reminded` | A reminder was sent to a signer |

---

## Webhook Payload

All deliveries use this envelope:

```json
{
  "id": "6f1d2c3a-....",
  "event": "document.signed",
  "timestamp": "2024-01-20T15:30:45+00:00",
  "attempt": 1,
  "data": {
    "document": {
      "id": 44,
      "title": "Employment Contract",
      "status": "sent"
    },
    "signer": {
      "id": 38,
      "name": "John Doe",
      "email": "john@example.com",
      "status": "signed"
    }
  },
  "meta": {
    "site_url": "https://your-site.com",
    "site_name": "Acme Corp",
    "plugin_version": "3.0.6",
    "webhook_id": "wh_abc123"
  }
}
```

### Payload Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique delivery ID (stable across retries) |
| `event` | string | Event type |
| `timestamp` | string | ISO 8601 timestamp (UTC) |
| `attempt` | integer | Delivery attempt number (1-based) |
| `data` | object | Event-specific nested objects (`document`, `signer`, etc.) |
| `meta` | object | Site/plugin context |

---

## Headers

Each delivery includes both preferred and legacy headers:

| Header | Description |
|--------|-------------|
| `X-WPS-Event` | Event name |
| `X-WPS-Delivery` | Delivery ID |
| `X-WPS-Timestamp` | Unix timestamp |
| `X-WPS-Attempt` | Attempt number |
| `X-WPS-Signature` | `sha256=<hmac>` when a secret is configured |
| `X-ESF-Event` | Legacy alias of `X-WPS-Event` |
| `X-ESF-Delivery` | Legacy alias of `X-WPS-Delivery` |
| `X-ESF-Timestamp` | Legacy alias of `X-WPS-Timestamp` |
| `X-ESF-Attempt` | Legacy alias of `X-WPS-Attempt` |
| `X-ESF-Signature` | Legacy alias of `X-WPS-Signature` |

Prefer `X-WPS-*` in new integrations. `X-ESF-*` remains for backward compatibility.

### Verifying Signatures

```
X-WPS-Signature: sha256=abc123...
```

### Verifying in PHP

```php
function verify_webhook_signature($payload, $signature, $secret) {
    $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}

$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WPS_SIGNATURE']
    ?? $_SERVER['HTTP_X_ESF_SIGNATURE']
    ?? '';
$secret = 'your_webhook_secret';

if (!verify_webhook_signature($payload, $signature, $secret)) {
    http_response_code(401);
    exit('Invalid signature');
}

$event = json_decode($payload, true);
```

### Verifying in Node.js

```javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
    const expected = 'sha256=' +
        crypto.createHmac('sha256', secret)
              .update(payload)
              .digest('hex');

    return crypto.timingSafeEqual(
        Buffer.from(expected),
        Buffer.from(signature)
    );
}

app.post('/webhooks/wpsigner', (req, res) => {
    const payload = req.rawBody; // use the exact raw body string
    const signature = req.headers['x-wps-signature'] || req.headers['x-esf-signature'];
    const secret = process.env.WEBHOOK_SECRET;

    if (!verifyWebhookSignature(payload, signature, secret)) {
        return res.status(401).send('Invalid signature');
    }

    res.status(200).send('OK');
});
```

---

## Responding to Webhooks

### Success Response

Return a `2xx` status code to acknowledge receipt:

```
HTTP/1.1 200 OK
```

### Failure Handling

If your endpoint returns a non-2xx status or times out:

1. WPsigner retries the webhook
2. Retry delays: **1 minute → 10 minutes → 60 minutes**
3. After 4 failed attempts, delivery stops (HTTP `410` from Zapier marks the hook inactive immediately)

### Timeout

Webhooks have a **15-second** timeout. For long-running processes, acknowledge first and process asynchronously:

```javascript
app.post('/webhooks/wpsigner', async (req, res) => {
    res.status(200).send('OK');
    processWebhookAsync(req.body);
});
```

---

## Best Practices

1. Always verify `X-WPS-Signature` (fallback to `X-ESF-Signature` if needed).
2. Respond quickly with `2xx`, then process asynchronously.
3. Use `id` (delivery ID) for idempotency.
4. Keep your endpoint on HTTPS.
5. Prefer nested `data.document` / `data.signer` fields from the live payload.

## Related

- [REST API Overview](/api/)
- [Documents API](/api/documents/)
