Skip to main content

Plutosign API documentation

Create, manage and send documents from your own tools. This reference is public; requests that access workspace data need a valid credential.

Download the OpenAPI specification

Authentication and plan access

Organization owners and admins can create API keys in Settings → API. Save the full key when it is shown; it is only displayed once. Send it in the Authorization header as a Bearer token. Keep API keys on your server.

API access and event webhooks are available on current plans; document allowances apply. Free allows 5 document creations or imports per monthly period. Starter and Business have unlimited document creation and sends. Document allowances belong to the workspace; AI allowances on paid plans scale with purchased seats.

In the app, document opens, views, signing timelines and organization-wide page viewing summaries are available on all current plans. Export document status, creation/sent/completion dates and recipient details as CSV.

OAuth access tokens use the zsign_at_ prefix and require the relevant read, write, send or admin scope. Organization API keys currently have full organization API permissions; treat them as privileged credentials.

curl https://plutosign.com/api/v1/documents \
  -H "Authorization: Bearer jsign_live_YOUR_API_KEY"

Create and send a document

Create a draft, then use its returned id in the send request. Review the content, recipient roles and field placement before sending. The send endpoint accepts recipientsList, with up to 50 recipients. Payment fields are currently unavailable.

curl -X POST https://plutosign.com/api/v1/documents \
  -H "Authorization: Bearer jsign_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Service Agreement","content":[{"type":"p","children":[{"text":"Your reviewed agreement goes here."}]}]}'
curl -X POST https://plutosign.com/api/v1/documents/{id}/send \
  -H "Authorization: Bearer jsign_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipientsList": [{"email":"client@example.com","name":"Your client","role":"signer"}],
    "fields": [{"type":"signature","recipientEmail":"client@example.com","page":1,"positionX":40,"positionY":120,"width":200,"height":50}],
    "message": "Please review and sign."
  }'

Use top-level signing links

The send response includes data.signingLinks with a url for each recipient. Share that recipient’s URL or open it in a top-level browser page. Recipients do not need a Plutosign account. Iframe embedding is currently unavailable. Use a normal signing link that opens a top-level page.

<!-- Use the recipient's URL from data.signingLinks; keep it private. -->
<a href="https://plutosign.com/sign/{token}" target="_blank" rel="noopener noreferrer">
  Review and sign
</a>
Common endpoints; see OpenAPI for full request and response schemas
MethodPathPurpose
GET/api/v1/documentsList documents
POST/api/v1/documentsCreate document
GET/api/v1/documents/{id}Get document
PATCH/api/v1/documents/{id}Update draft
DELETE/api/v1/documents/{id}Delete document, subject to evidence retention
POST/api/v1/documents/{id}/sendSend for signing
GET/api/v1/documents/{id}/downloadGet PDF download URL
GET/api/v1/documents/{id}/recipientsList recipients
GET/api/v1/templatesList templates
POST/api/v1/templatesCreate template
GET/api/v1/contactsList contacts
POST/api/v1/contactsCreate contact
GET/api/v1/webhooksList webhooks and event types
POST/api/v1/webhooksCreate webhook; save its secret

Register an event webhook

POST /api/v1/webhooks accepts a public HTTPS url and a non-empty events array. Save the whsec_ signing secret returned at creation; GET /api/v1/webhooks lists registrations without returning their secrets. OAuth tokens need admin scope for both operations.

curl -X POST https://plutosign.com/api/v1/webhooks \
  -H "Authorization: Bearer jsign_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.example/webhook","events":["document.completed","document.signed"]}'

Durable delivery is active for wired send, view, sign, decline, completion and expiry transitions. Events are persisted with the business transition; delivery starts on the next five-minute scheduler tick. Failed requests retry up to six total attempts, with minimum backoffs of 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. Each attempt has a 10-second timeout; redirects and unsafe destinations are rejected. Delivery is at least once, without ordering guarantees. Events emitted before activation have no retroactive guarantee. Delivery history is available at GET /api/v1/webhooks/deliveries; no delivery replay API is available.

  • document.created — Registered type; not currently emitted
  • document.sent — Document sent for signing
  • document.viewed — Document viewed
  • document.signed — Document signed
  • document.completed — Document completed
  • document.declined — Document declined
  • document.expired — Document expired
  • document.voided — Registered type; not currently emitted
  • recipient.signed — Recipient signed
  • recipient.viewed — Recipient viewed
  • recipient.declined — Recipient declined
  • payment.received — Reserved; payment collection is currently unavailable

The JSON payload has type, data, timestamp (the original event time) and eventId. Headers: X-ZSign-Signature: sha256=<hex digest>, X-ZSign-Event, X-ZSign-Timestamp and X-ZSign-Event-Id. Headers alone are unsigned metadata; compare them with the authenticated body. The User-Agent is ZSign-Webhooks/1.0.

Verify the raw body and prevent replay

Compute HMAC-SHA256 with the signing secret over the exact request bytes before JSON parsing. Re-serializing JSON changes those bytes. Use a constant-time comparison with a length check so malformed headers cannot throw.

The HMAC authenticates the exact raw body, including eventId and the original timestamp. Verify it before parsing, then require the event ID and timestamp headers to match the signed body. Atomically record the signed eventId per subscription with your processing result so retries cannot duplicate your work. Retain this record for the full replay horizon your integration supports. Retries preserve the original timestamp: backoff alone spans 14 hours and 36 minutes, and scheduler delays can extend it. A five-minute freshness limit rejects legitimate retries. The example accepts delayed authenticated events without an age cutoff and relies on durable deduplication; record receipt time separately.

const crypto = require('node:crypto');

function verifyWebhook(rawBody, signature, secret, idHeader, timestampHeader, typeHeader) {
  if (!Buffer.isBuffer(rawBody) || typeof signature !== 'string') return null;
  const expected = Buffer.from('sha256=' + crypto.createHmac('sha256', secret)
    .update(rawBody).digest('hex'));
  const received = Buffer.from(signature);
  if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) return null;
  let event;
  try { event = JSON.parse(rawBody.toString('utf8')); } catch { return null; }
  if (!event || typeof event.eventId !== 'string' ||
      !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(event.eventId) ||
      typeof event.timestamp !== 'string' || !Number.isFinite(Date.parse(event.timestamp)) ||
      typeof event.type !== 'string' || event.eventId !== idHeader ||
      event.timestamp !== timestampHeader || event.type !== typeHeader) return null;
  // No age cutoff: legitimate delayed retries keep the original timestamp.
  // Durable deduplication below is required before any side effect.
  return event;
}

// Mount this BEFORE express.json(); keep the exact bytes that were signed.
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  // Resolve this from your receiver configuration, never from request input.
  const subscription = YOUR_CONFIGURED_SUBSCRIPTION;
  const event = verifyWebhook(req.body, req.headers['x-zsign-signature'], subscription.secret,
    req.headers['x-zsign-event-id'], req.headers['x-zsign-timestamp'], req.headers['x-zsign-event']);
  if (!event) return res.status(401).send('Invalid signature or event metadata');
  // Implement in your database: a unique (subscription.id, event.eventId)
  // record and your work/outbox commit atomically. Duplicates return success.
  // Retain IDs for your full replay horizon; deleting them permits replay.
  await recordEventIdAndEnqueueOnce(subscription.id, event.eventId, event);
  return res.sendStatus(200);
});

Rate limits and integration boundaries

Document listing is limited to 120 requests per minute per credential; document creation to 60 and sending to 30. Webhook registration and listing share 60 requests per minute per IP. A rate-limited request returns 429 with X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Other endpoints have their own limits.

HubSpot and Pipedrive can import CRM contacts and create drafts from deals after you connect an account. Salesforce and full two-way CRM sync are not supported. In-document payment collection is currently unavailable. Use your existing invoicing or payment tool.

Compare current plans or contact support@plutosign.com to check your workflow.