Inkly API and webhooks

Send documents for signature from your own software, follow each one from viewed to signed, and pull the signed PDF back into your app. Included with Pro and Teams.

  • REST and JSON
  • Signed webhooks with retries
  • OpenAPI 3.1 document
On this page
  1. Getting started
  2. Authentication
  3. Requests and responses
  4. Idempotency
  5. Limits
  6. Errors
  7. Account
    1. getGet the current key
  8. Templates
    1. getList templates
    2. getGet a template
  9. Sending
    1. postSend a document from a template
    2. postStart a PDF upload
    3. postSend a document from an uploaded PDF
  10. Documents
    1. getList documents
    2. getGet a document
    3. postGet a signing link
    4. postSend a reminder
    5. postVoid a document
  11. Files
    1. getGet the signed PDF
    2. getDownload the Certificate of Completion
    3. getGet a signer attachment
  12. Reference
    1. getGet this API description
  13. Field markers
  14. Webhooks
    1. Events
    2. Payload
    3. Verifying signatures
    4. Retries and timing
    5. Handling duplicates and order
    6. Rolling a secret

Step-by-step setup guideOpenAPI document (JSON)

Getting started

There is no test mode. Every document you create is real and legally binding, counts toward your allowance, and sends completion copies. delivery: "link" only skips the invitation email, so it is not a sandbox. To try things out, send to your own email address.
  1. Create a key

    Requests need a Pro or Teams workspace. Go to API settings and click Create key. Inkly shows the key once, so store it somewhere safe.

  2. Make your first request

    Check your key
    curl "https://www.inkly.so/api/v1/me" \
      -H "Authorization: Bearer $INKLY_API_KEY"
  3. Send a document

    Use a template and fill its roles, or upload a PDF and place the fields yourself, by coordinates or with field markers.

  4. Find out when it is signed

    Add a webhook endpoint rather than polling.

Authentication

Send your key in the Authorization header as Bearer ink_live_…. A key in a URL is refused with api_key_in_query, because URLs end up in logs.

A key acts as the person who created it. The primary owner and Teams admins manage keys, and a workspace can have 10 live keys. The API sends no CORS headers, so call it from your server and never from a browser.

A Read key has
templates:readdocuments:read
A Read and send key has
templates:readdocuments:readdocuments:send

Requests and responses

The base URL is https://www.inkly.so/api/v1. Bodies are JSON, sent with Content-Type: application/json, and can be up to 64 KB.

Unknown fields in a request body or list query are refused with unknown_field, so a typo cannot pass silently. Responses can gain new fields without notice, so ignore the ones you do not recognize.

Lists return newest first. When next_cursor is not null, pass it as cursor to get the next page. limit is 1 to 100, and defaults to 25.

Every response from the API carries an Inkly-Request-Id header. Include it if you contact support.

Idempotency

The two send endpoints require an Idempotency-Key header: any unique value you choose, 1 to 255 printable ASCII characters. If a request times out, retry it with the same key and you get the first result rather than a second document.

  • A replayed response carries Idempotent-Replayed: true, including a replayed error.
  • 5xx and 429 outcomes are not stored, so the same key can retry them.
  • Keys are unique per workspace and endpoint, shared across that workspace's API keys, and kept for 24 hours.
  • The same key with a different body returns idempotency_key_reused.
  • Other endpoints refuse the header with idempotency_not_supported.

Limits

A workspace can create 250 documents a month through the API on Pro and 1,000 on Teams, resetting on the 1st at 00:00 UTC. Past that, sends return 429 monthly_allowance_exceeded. A plan change applies straight away to what the workspace has already used this month. Only documents created through the API count: reads, signing links, reminders, and voids do not, and documents sent from the Inkly app stay unlimited.

Requests per IP address
300 a minute
Requests per key
120 a minute
Requests per workspace
300 a minute, 20,000 a day
Document sends per workspace
20 a minute
Signing links per workspace
300 an hour
Signed PDF downloads
100 an hour
Certificate downloads
30 an hour
Attachment downloads per key
100 an hour
PDF upload
20 MB, 500 pages, 1 hour to use
One document
25 recipients, 5 of them CCs, 500 fields

A 429 always carries Retry-After, in seconds. It usually also carries RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. Other responses do not carry these headers, except that 409 upload_in_progress also carries Retry-After.

The per-IP limit is enforced by the network in front of the API. Its 429 is plain text, with no JSON error body and none of these headers, and no Inkly-Request-Id. Wait up to a minute before retrying, and handle a 429 whose body is not JSON.

Errors

Errors use the HTTP status and this body. Branch on code, which is stable. message can change.

Error body
{
  "error": {
    "code": "recipient_invalid_email",
    "message": "The document could not be sent: recipient invalid email.",
    "param": "recipients.0.email",
    "request_id": "req_4f9a1c7e2b8d4a60"
  }
}

Any endpoint can return these:

  • invalid_api_key401Provide a valid API key in the Authorization header.
  • api_key_in_query400Send API keys in the Authorization header, never in a URL.
  • api_disabled403 or 503API access is disabled for this workspace (403) or for everyone for now (503).
  • plan_required403API access requires Pro or Teams.
  • scope_required403This key does not have the required scope.
  • rate_limited429Too many requests. Try again after the indicated reset time.
  • idempotency_not_supported400This endpoint does not support Idempotency-Key.
  • internal_error500The request could not be completed. Try again.
Every error code (63)

400 Bad request

  • api_key_in_query400Send API keys in the Authorization header, never in a URL.
  • dropdown_options_invalid400A dropdown's options are missing, duplicated, or not valid.
  • field_out_of_bounds400A field falls outside the page.
  • idempotency_key_required400Provide an Idempotency-Key header.
  • idempotency_not_supported400This endpoint does not support Idempotency-Key.
  • invalid_request400The request is invalid.
  • invite_message_too_long400The invitation message is too long.
  • prefilled_field_invalid_label400A prefilled field's label is not valid.
  • prefilled_field_invalid_value400A prefilled value contains text that cannot be used.
  • prefilled_value_conflict400Prefilled fields with the same label must have the same value.
  • recipient_invalid_email400A recipient's email address is not valid.
  • recipient_missing_email400A recipient has no email address.
  • unknown_field400This field is not supported.
  • unknown_prefilled_field400This prefilled field is not in the template.

401 and 403 Authentication and access

  • invalid_api_key401Provide a valid API key in the Authorization header.
  • api_disabled403 or 503API access is disabled.
  • plan_required403API access requires Pro or Teams.
  • scope_required403This key does not have the required scope.
  • sending_paused403Sending is paused for this workspace. Contact support.
  • storage_limit_reached403This workspace has reached its document storage limit. Upgrade your plan or email hello@inkly.so for more storage, then retry with a new Idempotency-Key.
  • void_not_allowed403Only the document's creator or a workspace admin can void it.

404 and 410 Not found

  • attachment_not_found404Attachment not found.
  • document_not_found404Document not found.
  • template_not_found404Template not found.
  • upload_not_found404Upload not found.
  • upload_expired410This upload has expired. Create a new upload and try again.

409 Conflict

  • certificate_not_ready409The certificate is not ready. Retry shortly.
  • document_changed409The document or template changed while it was being sent. Retry with a new Idempotency-Key.
  • document_not_completed409This document is not completed.
  • document_not_voidable409Only sent documents can be voided.
  • idempotency_key_reused409This idempotency key was used with a different request.
  • idempotency_request_in_progress409A request with this idempotency key is still running.
  • reminder_cooldown409A reminder is already in progress or cooling down.
  • reminder_not_available409Reminders are not available for this document.
  • signed_pdf_not_ready409The signed PDF is not ready. Retry shortly.
  • upload_already_used409This upload has already been used to send a document.
  • upload_in_progress409Another request is processing this upload. Retry shortly.
  • upload_incomplete409The PDF has not been uploaded yet. PUT the file to upload_url, then retry.

413 and 422 Content that cannot be used

  • body_too_large413The JSON body must be at most 64 KB.
  • cc_recipient_cannot_have_fields422A CC recipient cannot be assigned fields.
  • cc_recipient_duplicate_email422The same CC email address appears more than once.
  • cc_recipient_limit_exceeded422A document can have at most 5 CC recipients.
  • document_has_no_fields422The document has no fields.
  • document_has_no_recipients422The document has no recipients.
  • document_has_no_signers422The document has no signers.
  • google_drive_not_connected422The template saves to Google Drive, but Drive is not connected.
  • in_person_not_supported422Templates with in-person signers cannot be sent through the API.
  • invalid_pdf422The uploaded file is not a valid PDF.
  • marker_invalid422A field marker is malformed, wraps onto another line, starts in the middle of other text, does not fit on its page, or takes the document over 500 fields.
  • marker_not_found422use_markers is true, but the PDF has no field markers Inkly can read. A scanned PDF has no text, so send fields with coordinates instead.
  • marker_page_unsupported422A page with a field marker uses text or layout Inkly cannot place a field on exactly. Fix the PDF, or send fields with coordinates instead.
  • marker_pdf_too_complex422The PDF is too large or complex for marker detection. Send fields with coordinates instead.
  • marker_unknown_recipient422A field marker names a signer that is not a signer ref in this request.
  • pdf_too_many_pages422The PDF has more than 500 pages.
  • prefilled_value_missing422A required prefilled field has no value.
  • recipient_limit_exceeded422A document can have at most 25 recipients.
  • selection_group_incomplete422A checkbox or radio group on the template is incomplete.
  • signer_has_no_fields422A signer has no field to fill in.

429 Too many requests

  • monthly_allowance_exceeded429The monthly API document allowance has been reached.
  • rate_limited429Too many requests. Try again after the indicated reset time.

500 and 503 Server errors

  • internal_error500The request could not be completed. Try again.
  • temporarily_unavailable503The service is temporarily unavailable. Try again.

Account

Check which workspace a key belongs to and how much of the monthly allowance is left.

Get the current key

get/me

Returns the key, its workspace, and the current monthly allowance. Any valid key can call this, so it is a good first request.

Works with any valid key.

Example request
curl "https://www.inkly.so/api/v1/me" \
  -H "Authorization: Bearer $INKLY_API_KEY"
Example response, 200
{
  "key": {
    "name": "CRM integration",
    "last_four": "x7Qa",
    "scopes": [
      "templates:read",
      "documents:read",
      "documents:send"
    ]
  },
  "workspace": {
    "id": "6c0e2b91-5d47-4a83-b2f6-7e9a1c3d8f50",
    "name": "Northwind Studio",
    "plan": "pro"
  },
  "allowance": {
    "limit": 250,
    "used": 12,
    "remaining": 238,
    "resets_at": "2026-12-01T00:00:00.000Z"
  }
}
Response fields
  • keyobjectRequired
    3 child fields
    • namestringRequired
    • last_fourstringRequired
    • scopesarray of stringsRequired

      Each one of templates:read, documents:read, documents:send

  • workspaceobjectRequired
    3 child fields
    • idUUIDRequired
    • namestringRequired
    • planstringRequired

      One of pro, teams

  • allowanceobjectRequired
    4 child fields
    • limitnumberRequired

      API documents this workspace can create each month, which depends on its plan.

    • usednumberRequired
    • remainingnumberRequired
    • resets_attimestampRequired

      The first of next month, UTC.

Templates

Find a template and the roles you need to fill to send it.

List templates

get/templates

Lists the templates this key can see, newest first.

Needs a key with templates:read.

Query parameters

  • limitnumber

    How many results to return per page.

    1 to 100 · Default 25

  • cursorstring

    The next_cursor value from the previous page. Treat it as opaque.

Example request
curl "https://www.inkly.so/api/v1/templates?limit=25" \
  -H "Authorization: Bearer $INKLY_API_KEY"
Example response, 200
{
  "templates": [
    {
      "id": "a2d94c10-6e7b-4f38-8c15-1f3e9b7d6c02",
      "title": "Consulting agreement",
      "created_at": "2026-11-02T15:04:05.123Z"
    }
  ],
  "next_cursor": null
}
Response fields
  • templatesarray of objectsRequired
    3 child fields
    • idUUIDRequired
    • titlestringRequired
    • created_attimestampRequired
  • next_cursorstring or nullRequired

    Pass as cursor to get the next page. null on the last page.

Errors specific to this endpoint

Every endpoint can also return the common errors.

Get a template

get/templates/{id}

Returns the roles you must fill and the prefilled fields you can set when you send this template.

Needs a key with templates:read.

Path parameters

  • idUUIDRequired

    The template ID.

Example request
curl "https://www.inkly.so/api/v1/templates/a2d94c10-6e7b-4f38-8c15-1f3e9b7d6c02" \
  -H "Authorization: Bearer $INKLY_API_KEY"
Example response, 200
{
  "template": {
    "id": "a2d94c10-6e7b-4f38-8c15-1f3e9b7d6c02",
    "title": "Consulting agreement",
    "created_at": "2026-11-02T15:04:05.123Z",
    "signing_order": "parallel",
    "roles": [
      {
        "role_id": "c81e7f42-0b39-4d6a-a7c4-92e15d3f8b76",
        "label": "Client",
        "role": "signer",
        "in_person": false,
        "file_requests": []
      }
    ],
    "prefilled_fields": [
      {
        "label": "Project name",
        "key": "project name",
        "required": true,
        "type": "text"
      }
    ]
  }
}
Response fields
  • templateobjectRequired
    6 child fields
    • idUUIDRequired
    • titlestringRequired
    • created_attimestampRequired
    • signing_orderstringRequired

      One of parallel, sequential

    • rolesarray of objectsRequired
      5 child fields
      • role_idUUIDRequired

        Use this in recipients[].role_id when you send.

      • labelstringRequired
      • rolestringRequired

        One of signer, cc

      • in_personbooleanRequired

        Templates with in-person signers cannot be sent through the API.

      • file_requestsarray of objectsRequired

        Files this signer is asked to upload.

        3 child fields
        • namestringRequired
        • requiredbooleanRequired
        • positionnumberRequired
    • prefilled_fieldsarray of objectsRequired

      Fields you fill in when you send, through prefilled_values.

      4 child fields
      • labelstringRequired
      • keystringRequired

        The normalized label.

      • requiredbooleanRequired
      • typestringRequired
Errors specific to this endpoint

Every endpoint can also return the common errors.

Sending

Send a document from a template or from a PDF you upload. Both create a real, legally binding document.

Send a document from a template

post/documents

Creates and sends a real document from a template. recipients must fill every role on the template exactly once. Templates with in-person signers are refused. Each document counts toward the monthly allowance.

Needs a key with documents:send. Requires an Idempotency-Key header.

Request body

  • template_idUUIDRequired

    The template to send.

  • titlestring

    The document title. Defaults to the template's title.

    1 to 120 characters

  • external_idstring

    Your own reference for this document, in printable ASCII. It is not required to be unique.

    Up to 255 characters

  • recipientsarray of objectsRequired

    One entry for every template role, each role exactly once.

    1 to 25 items

    4 child fields
    • namestring

      The person's name.

      Up to 200 characters

    • emailemailRequired

      The person's email address.

    • role_idUUIDRequired

      The template role this person fills. Get it from GET /templates/{id}.

    • deliverystring

      email sends the invitation and reminders. link sends neither and gives you signing_url to share yourself. It is not a sandbox: the document is real and legally binding.

      One of email, link

      Default email

  • ccarray of objects

    Extra people who get a copy once the document is complete.

    Up to 5 items

    2 child fields
    • namestring

      The person's name.

      Up to 200 characters

    • emailemailRequired

      The person's email address.

  • prefilled_valuesobject

    Values for the template's prefilled fields, keyed by field label.

  • messagestring

    A personal note in the invitation.

    Up to 2000 characters

  • automatic_remindersboolean

    Send automatic reminders until the document is signed.

    Default false

Example request
curl -X POST "https://www.inkly.so/api/v1/documents" \
  -H "Authorization: Bearer $INKLY_API_KEY" \
  -H "Idempotency-Key: crm-8841-send-1" \
  -H "Content-Type: application/json" \
  -d '{
  "template_id": "a2d94c10-6e7b-4f38-8c15-1f3e9b7d6c02",
  "title": "Consulting agreement",
  "external_id": "crm-8841",
  "recipients": [
    {
      "role_id": "c81e7f42-0b39-4d6a-a7c4-92e15d3f8b76",
      "name": "Alex Morgan",
      "email": "alex@example.com",
      "delivery": "email"
    }
  ],
  "cc": [
    {
      "name": "Sam Lee",
      "email": "sam@example.com"
    }
  ],
  "prefilled_values": {
    "Project name": "Website redesign"
  },
  "message": "Please sign by Friday."
}'
Example response, 201
{
  "document": {
    "id": "3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34",
    "title": "Consulting agreement",
    "status": "sent",
    "external_id": "crm-8841",
    "template_id": "a2d94c10-6e7b-4f38-8c15-1f3e9b7d6c02",
    "created_at": "2026-11-02T15:04:05.123Z"
  },
  "recipients": [
    {
      "id": "5b7e9d13-2c84-4f06-b1a9-6d40e8c2f715",
      "role_id": "c81e7f42-0b39-4d6a-a7c4-92e15d3f8b76",
      "role": "signer",
      "name": "Alex Morgan",
      "email": "alex@example.com",
      "delivery": "email",
      "status": "pending",
      "signing_url": null
    },
    {
      "id": "e47a0c96-7d21-4b58-9f3e-08b6c1d5a243",
      "role_id": null,
      "role": "cc",
      "name": "Sam Lee",
      "email": "sam@example.com",
      "delivery": "email",
      "status": "pending",
      "signing_url": null
    }
  ]
}
Response fields
  • documentobjectRequired
    6 child fields
    • idUUIDRequired
    • titlestringRequired
    • statusstringRequired

      One of draft, sent, completed, declined, voided

    • external_idstring or nullRequired
    • template_idUUID or nullRequired
    • created_attimestampRequired
  • recipientsarray of objectsRequired
    8 child fields
    • idUUIDRequired
    • rolestringRequired

      One of signer, cc

    • namestring or nullRequired
    • emailstring or nullRequired
    • deliverystringRequired

      How this person hears from Inkly. Always email for CC recipients.

      One of email, link

    • statusstringRequired

      One of pending, viewed, signed, declined

    • signing_urlURL or nullRequired

      Set only when delivery is link and it is this signer's turn. With sequential signing, later signers get null: when GET /documents/{id} shows is_signing_turn is true, call POST /documents/{id}/recipients/{recipient_id}/signing-link. Treat it like a password: anyone holding it can sign.

    • role_idUUID or nullRequired

      The template role. null for people added through cc.

Errors specific to this endpoint

Every endpoint can also return the common errors.

Start a PDF upload

post/uploads

Reserves a one-hour upload for a PDF of up to 20 MB and 500 pages. Then send the file with PUT to upload_url, setting Content-Type: application/pdf and no API key, and pass upload_id to POST /documents/from-upload.

Needs a key with documents:send.

Request body

  • file_namestringRequired

    Must end in .pdf.

  • byte_sizenumberRequired

    The exact size of the file you will upload, in bytes.

Example request
curl -X POST "https://www.inkly.so/api/v1/uploads" \
  -H "Authorization: Bearer $INKLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "file_name": "consulting-agreement.pdf",
  "byte_size": 482133
}'
Example response, 201
{
  "upload_id": "d06b8e24-9f13-4c7a-a5d8-3e1c7f2b6049",
  "upload_url": "https://files.example.com/upload/consulting-agreement.pdf?token=example",
  "expires_at": "2026-11-02T16:04:05.123Z"
}
Response fields
  • upload_idUUIDRequired
  • upload_urlURLRequired

    PUT the PDF here. The URL is its own credential, so send no API key.

  • expires_attimestampRequired

    One hour after the upload was created.

Errors specific to this endpoint

Every endpoint can also return the common errors.

Send a document from an uploaded PDF

post/documents/from-upload

Creates and sends a real document from a PDF you uploaded, placing fields by page and position. x, y, width, and height are fractions of the page measured from its top-left corner. Or set use_markers to true and write fields into the PDF as text such as {{signature;signer=client}}: Inkly creates a field at each marker, never changes the PDF, and refuses the whole send if a marker is wrong or its position cannot be read exactly. See Field markers for the syntax and how to hide a marker. Prefilled fields must be text. Each document counts toward the monthly allowance.

Needs a key with documents:send. Requires an Idempotency-Key header.

Request body

  • upload_idUUIDRequired

    From POST /uploads, after the PDF has been uploaded.

  • titlestring

    The document title. Defaults to the file name.

    1 to 120 characters

  • external_idstring

    Your own reference for this document, in printable ASCII. It is not required to be unique.

    Up to 255 characters

  • signing_orderstring

    sequential invites signers one at a time, in the order listed.

    One of parallel, sequential

    Default parallel

  • messagestring

    A personal note in the invitation.

    Up to 2000 characters

  • automatic_remindersboolean

    Send automatic reminders until the document is signed.

    Default false

  • recipientsarray of one of the types belowRequired

    Up to 25 people, at most 5 of them CCs.

    1 to 25 items

    2 types

    Signer

    • namestring

      The person's name.

      Up to 200 characters

    • emailemailRequired

      The person's email address.

    • refstringRequired

      Your own short name for this person, unique in this request. Fields point at it.

      1 to 40 characters

    • role"signer"

      Default signer

    • deliverystring

      email sends the invitation and reminders. link sends neither and gives you signing_url to share yourself. It is not a sandbox: the document is real and legally binding.

      One of email, link

      Default email

    CC

    • namestring

      The person's name.

      Up to 200 characters

    • emailemailRequired

      The person's email address.

    • refstringRequired

      Your own short name for this person, unique in this request. Fields point at it.

      1 to 40 characters

    • role"cc"Required
  • use_markersboolean

    Create fields from {{...}} markers in the PDF. fields then becomes optional, and any you send are added to the detected ones.

    Default false

  • fieldsarray of objects

    Each signer needs at least one field they fill in. Required unless use_markers is true.

    Up to 500 items

    12 child fields
    • recipientstringRequired

      The ref of the signer who fills this field.

      1 to 40 characters

    • typestringRequired

      One of signature, initials, name, email, date, text, number, checkbox, dropdown

    • pagenumberRequired

      The page number, starting at 1.

    • xnumberRequired

      Distance from the page's left edge, as a fraction of its width.

      0 to 1

    • ynumberRequired

      Distance from the page's top edge, as a fraction of its height.

      0 to 1

    • widthnumberRequired

      A fraction of the page width. x + width can be at most 1.

    • heightnumberRequired

      A fraction of the page height. y + height can be at most 1.

    • labelstring

      The label the signer sees.

    • requiredboolean

      Default true

    • optionsarray of strings

      The choices, for a dropdown.

    • prefilledboolean

      You fill this field in now, and it locks. Text fields only.

      Default false

    • prefill_valuestring

      The value of a prefilled field.

Example request
curl -X POST "https://www.inkly.so/api/v1/documents/from-upload" \
  -H "Authorization: Bearer $INKLY_API_KEY" \
  -H "Idempotency-Key: crm-8841-send-1" \
  -H "Content-Type: application/json" \
  -d '{
  "upload_id": "d06b8e24-9f13-4c7a-a5d8-3e1c7f2b6049",
  "title": "Consulting agreement",
  "external_id": "crm-8841",
  "signing_order": "parallel",
  "recipients": [
    {
      "ref": "client",
      "name": "Alex Morgan",
      "email": "alex@example.com",
      "delivery": "link"
    },
    {
      "ref": "finance",
      "role": "cc",
      "name": "Sam Lee",
      "email": "sam@example.com"
    }
  ],
  "fields": [
    {
      "recipient": "client",
      "type": "signature",
      "page": 1,
      "x": 0.1,
      "y": 0.8,
      "width": 0.35,
      "height": 0.06
    },
    {
      "recipient": "client",
      "type": "date",
      "page": 1,
      "x": 0.55,
      "y": 0.8,
      "width": 0.2,
      "height": 0.04
    }
  ]
}'
Example response, 201
{
  "document": {
    "id": "3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34",
    "title": "Consulting agreement",
    "status": "sent",
    "external_id": "crm-8841",
    "template_id": null,
    "created_at": "2026-11-02T15:04:05.123Z"
  },
  "recipients": [
    {
      "id": "5b7e9d13-2c84-4f06-b1a9-6d40e8c2f715",
      "ref": "client",
      "role": "signer",
      "name": "Alex Morgan",
      "email": "alex@example.com",
      "delivery": "link",
      "status": "pending",
      "signing_url": "https://www.inkly.so/documents/3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34/sign/example-signing-token"
    },
    {
      "id": "e47a0c96-7d21-4b58-9f3e-08b6c1d5a243",
      "ref": "finance",
      "role": "cc",
      "name": "Sam Lee",
      "email": "sam@example.com",
      "delivery": "email",
      "status": "pending",
      "signing_url": null
    }
  ]
}
Response fields
  • documentobjectRequired
    6 child fields
    • idUUIDRequired
    • titlestringRequired
    • statusstringRequired

      One of draft, sent, completed, declined, voided

    • external_idstring or nullRequired
    • template_idUUID or nullRequired
    • created_attimestampRequired
  • recipientsarray of objectsRequired
    8 child fields
    • idUUIDRequired
    • rolestringRequired

      One of signer, cc

    • namestring or nullRequired
    • emailstring or nullRequired
    • deliverystringRequired

      How this person hears from Inkly. Always email for CC recipients.

      One of email, link

    • statusstringRequired

      One of pending, viewed, signed, declined

    • signing_urlURL or nullRequired

      Set only when delivery is link and it is this signer's turn. With sequential signing, later signers get null: when GET /documents/{id} shows is_signing_turn is true, call POST /documents/{id}/recipients/{recipient_id}/signing-link. Treat it like a password: anyone holding it can sign.

    • refstringRequired

      The ref you sent for this person.

Errors specific to this endpoint

Every endpoint can also return the common errors.

Documents

Track documents and act on them after they are sent.

List documents

get/documents

Lists documents in the workspace, newest first, including those sent from the app.

Needs a key with documents:read.

Query parameters

  • limitnumber

    How many results to return per page.

    1 to 100 · Default 25

  • cursorstring

    The next_cursor value from the previous page. Treat it as opaque.

  • statusstring

    Only documents with this status.

    One of draft, sent, completed, declined, voided

  • template_idUUID

    Only documents sent from this template.

  • external_idstring

    Only documents with this external_id.

    Up to 255 characters

  • signing_link_idUUID

    Only documents started from this public signing link.

Example request
curl "https://www.inkly.so/api/v1/documents?limit=25&status=completed" \
  -H "Authorization: Bearer $INKLY_API_KEY"
Example response, 200
{
  "documents": [
    {
      "id": "3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34",
      "title": "Consulting agreement",
      "status": "sent",
      "external_id": "crm-8841",
      "template_id": "a2d94c10-6e7b-4f38-8c15-1f3e9b7d6c02",
      "source": "api",
      "signing_link_id": null,
      "created_at": "2026-11-02T15:04:05.123Z",
      "sent_at": "2026-11-02T15:04:05.123Z",
      "completed_at": null,
      "voided_at": null,
      "archived_at": null
    }
  ],
  "next_cursor": null
}
Response fields
  • documentsarray of objectsRequired
    12 child fields
    • idUUIDRequired
    • titlestringRequired
    • statusstringRequired

      One of draft, sent, completed, declined, voided

    • external_idstring or nullRequired
    • template_idUUID or nullRequired
    • sourcestringRequired

      One of api, app, bulk, link

    • signing_link_idUUID or nullRequired
    • created_attimestampRequired
    • sent_attimestamp or nullRequired
    • completed_attimestamp or nullRequired
    • voided_attimestamp or nullRequired
    • archived_attimestamp or nullRequired
  • next_cursorstring or nullRequired
Errors specific to this endpoint

Every endpoint can also return the common errors.

Get a document

get/documents/{id}

Returns the document and each recipient's progress. fields appears only once the document is complete.

Needs a key with documents:read.

Path parameters

  • idUUIDRequired

    The document ID.

Example request
curl "https://www.inkly.so/api/v1/documents/3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34" \
  -H "Authorization: Bearer $INKLY_API_KEY"
Example response, 200
{
  "document": {
    "id": "3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34",
    "title": "Consulting agreement",
    "status": "sent",
    "external_id": "crm-8841",
    "template_id": "a2d94c10-6e7b-4f38-8c15-1f3e9b7d6c02",
    "source": "api",
    "signing_link_id": null,
    "created_at": "2026-11-02T15:04:05.123Z",
    "sent_at": "2026-11-02T15:04:05.123Z",
    "completed_at": null,
    "voided_at": null,
    "archived_at": null,
    "recipients": [
      {
        "id": "5b7e9d13-2c84-4f06-b1a9-6d40e8c2f715",
        "ref": null,
        "role": "signer",
        "name": "Alex Morgan",
        "email": "alex@example.com",
        "delivery": "email",
        "status": "viewed",
        "signing_step": null,
        "is_signing_turn": true,
        "viewed_at": "2026-11-02T15:04:05.123Z",
        "signed_at": null,
        "declined_at": null,
        "decline_reason": null,
        "invite_delivery": "sent",
        "attachments": []
      }
    ]
  }
}
Response fields
  • documentobjectRequired
    14 child fields
    • idUUIDRequired
    • titlestringRequired
    • statusstringRequired

      One of draft, sent, completed, declined, voided

    • external_idstring or nullRequired
    • template_idUUID or nullRequired
    • sourcestringRequired

      One of api, app, bulk, link

    • signing_link_idUUID or nullRequired
    • created_attimestampRequired
    • sent_attimestamp or nullRequired
    • completed_attimestamp or nullRequired
    • voided_attimestamp or nullRequired
    • archived_attimestamp or nullRequired
    • recipientsarray of objectsRequired
      15 child fields
      • idUUIDRequired
      • refstring or nullRequired
      • rolestringRequired

        One of signer, cc

      • namestring or nullRequired
      • emailstring or nullRequired
      • deliverystringRequired

        One of email, link

      • statusstringRequired

        One of pending, viewed, signed, declined

      • signing_stepnumber or nullRequired
      • is_signing_turnbooleanRequired
      • viewed_attimestamp or nullRequired
      • signed_attimestamp or nullRequired
      • declined_attimestamp or nullRequired
      • decline_reasonstring or nullRequired
      • invite_deliverystring or nullRequired

        One of pending, sending, sent, failed, bounced, complained, suppressed

      • attachmentsarray of objectsRequired
        6 child fields
        • idUUIDRequired
        • file_namestringRequired
        • content_typestringRequired

          One of application/pdf, image/jpeg, image/png

        • sizenumberRequired
        • uploaded_attimestampRequired
        • request_labelstring or nullRequired
    • fieldsarray of objects
      6 child fields
      • idUUIDRequired
      • recipient_idUUIDRequired
      • typestringRequired

        One of signature, initials, name, email, date, text, number, checkbox, dropdown

      • labelstringRequired
      • valueany or nullRequired
      • filledboolean
Errors specific to this endpoint

Every endpoint can also return the common errors.

Send a reminder

post/documents/{id}/remind

Emails a reminder to everyone who still has to sign. Recipients with delivery: "link" get no reminders.

Needs a key with documents:send. Send no body, or an empty {}.

Path parameters

  • idUUIDRequired

    The document ID.

Example request
curl -X POST "https://www.inkly.so/api/v1/documents/3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34/remind" \
  -H "Authorization: Bearer $INKLY_API_KEY"
Example response, 200
{
  "sent_count": 1,
  "skipped_count": 0,
  "failed_count": 0
}
Response fields
  • sent_countnumberRequired
  • skipped_countnumberRequired
  • failed_countnumberRequired
Errors specific to this endpoint

Every endpoint can also return the common errors.

Void a document

post/documents/{id}/void

Cancels a sent document so nobody else can sign it. This cannot be undone.

Needs a key with documents:send. Send no body, or an empty {}.

Path parameters

  • idUUIDRequired

    The document ID.

Example request
curl -X POST "https://www.inkly.so/api/v1/documents/3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34/void" \
  -H "Authorization: Bearer $INKLY_API_KEY"
Example response, 200
{
  "document": {
    "id": "3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34",
    "status": "voided",
    "voided_at": "2026-11-02T15:04:05.123Z"
  }
}
Response fields
  • documentobjectRequired
    3 child fields
    • idUUIDRequired
    • status"voided"Required
    • voided_attimestampRequired
Errors specific to this endpoint

Every endpoint can also return the common errors.

Files

Download what a completed document produced. Links work for 5 minutes, so fetch a new one each time.

Get the signed PDF

get/documents/{id}/signed-pdf

Returns a 5-minute download link for a completed document's signed PDF.

Needs a key with documents:read.

Path parameters

  • idUUIDRequired

    The document ID.

Example request
curl "https://www.inkly.so/api/v1/documents/3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34/signed-pdf" \
  -H "Authorization: Bearer $INKLY_API_KEY"
Example response, 200
{
  "url": "https://files.example.com/signed/consulting-agreement.pdf?token=example",
  "expires_at": "2026-11-02T15:09:05.123Z",
  "file_name": "Consulting agreement.pdf"
}
Response fields
  • urlURLRequired

    A signed download URL. It works for 5 minutes and needs no API key.

  • expires_attimestampRequired
  • file_namestringRequired
Errors specific to this endpoint

Every endpoint can also return the common errors.

Download the Certificate of Completion

get/documents/{id}/certificate

Returns the PDF itself rather than a link.

Needs a key with documents:read.

Path parameters

  • idUUIDRequired

    The document ID.

Example request
curl "https://www.inkly.so/api/v1/documents/3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34/certificate" \
  -H "Authorization: Bearer $INKLY_API_KEY" \
  --output certificate.pdf

Returns 200 with the PDF as the response body.

Errors specific to this endpoint

Every endpoint can also return the common errors.

Get a signer attachment

get/documents/{id}/attachments/{attachment_id}

Returns a 5-minute download link for a file a signer uploaded.

Needs a key with documents:read.

Path parameters

  • idUUIDRequired

    The document ID.

  • attachment_idUUIDRequired

    The attachment ID, from the recipient's attachments.

Example request
curl "https://www.inkly.so/api/v1/documents/3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34/attachments/91c3f5e8-4a07-4d92-8b6e-2f7d1a0c9e35" \
  -H "Authorization: Bearer $INKLY_API_KEY"
Example response, 200
{
  "url": "https://files.example.com/signed/consulting-agreement.pdf?token=example",
  "expires_at": "2026-11-02T15:09:05.123Z",
  "file_name": "Proof of insurance.pdf"
}
Response fields
  • urlURLRequired

    A signed download URL. It works for 5 minutes and needs no API key.

  • expires_attimestampRequired
  • file_namestringRequired
Errors specific to this endpoint

Every endpoint can also return the common errors.

Reference

This API description, as a machine-readable document.

Get this API description

get/openapi.json

Returns this API as an OpenAPI 3.1 document, for generating clients or loading into API tools. It needs no key.

No key needed.

Example request
curl "https://www.inkly.so/api/v1/openapi.json"
Example response, 200
{
  "openapi": "3.1.0",
  "info": {
    "title": "Inkly API",
    "version": "1.0.0"
  }
}

Field markers

Instead of measuring coordinates, write each field into the document as a marker and send the PDF to POST /documents/from-upload with use_markers set to true. Inkly creates a field at each marker. Inkly never changes your PDF, and if a marker is wrong or its position cannot be read exactly, the whole send is refused and nothing goes out.

A marker in the document
Signed by: {{signature;signer=client}}
The send request
{
  "upload_id": "...",
  "use_markers": true,
  "recipients": [{ "ref": "client", "name": "Alex Morgan", "email": "alex@example.com" }]
}

You can combine markers with fields, up to 500 fields in total. Dropdown and prefilled fields still go in fields. Without use_markers the PDF is not scanned for markers.

Syntax

Marker syntax
{{<type>;signer=<ref>[;required=false][;width=<fraction>][;height=<fraction>]}}
  • typeFirst, and case-insensitive: signature, initials, name, email, date, text, number, or checkbox. A dropdown needs options, so send it in fields.
  • signerRequired. The exact ref of a recipient with the signer role. A ref containing ; = { or } cannot be used in a marker.
  • requiredOptional: true or false. Defaults to true.
  • width, heightOptional: a decimal above 0 and up to 1, as a fraction of the page. Defaults are in the next section.

Option names are lowercase, each can appear once, and unknown options are refused. Spaces around ; and = are ignored. A marker can be up to 200 characters and must fit on one line.

Hiding a marker

Make the marker white and set it at 1 or 2 points, so its size differs from the text around it. A marker the same size as the text beside it may be merged into that text by the tool that exports the PDF, and is then refused. We tested this with pdf-lib, Chrome print to PDF, and Google Docs. Word has not been tested directly.

Any hidden text works, such as text render mode 3 or zero opacity, which also suits colored backgrounds.

Marker color is not checked, so a marker left black will be visible to the signer. White text also stays in the PDF, where it can be found by text search or copying.

Where the field goes

The field starts at the marker's left edge, and its bottom sits on the marker's baseline, so it grows up from a signature line. A box that would run off the page is moved back onto it. Default sizes, in PDF points:

  • signature180 × 40
  • initials72 × 28
  • name, email, text180 × 20
  • date, number110 × 20
  • checkbox14 × 14

Inkly does not detect a field overlapping other text, so leave blank space around each marker and check the result by sending to your own email address.

What cannot be read

Inkly can only act on markers the PDF exposes as text, so a marker in a scan or an image, or one lost by the exporting tool, is not found. A marker must be plain ASCII and kept apart from right-to-left text such as Hebrew or Arabic.

A page containing a marker is refused as marker_page_unsupported if it has any of these anywhere on the page, even far from the marker, because Inkly cannot place a field on it exactly:

  • Page rotation, a user unit other than 1, or a visible area that does not start at the page's origin
  • Rotated, skewed, or mirrored marker text
  • Soft hyphens, zero-width or other invisible formatting characters, or accents drawn as separate glyphs
  • Character spacing or kerning adjustments
  • Type3 or vertical fonts, zero-size text, a broken font, or a font the PDF does not include other than the standard 14

Recovering from a refusal

  • marker_not_foundNo marker could be read. A scanned PDF has no text, so a scan needs coordinates. Fix the PDF and upload it again, or send fields with a new key on the same upload.
  • marker_invalidA marker is malformed, wraps onto another line, starts in the middle of other text, does not fit on its page, or takes the document over 500 fields. Fix the PDF, upload it again, and use a new key.
  • marker_unknown_recipientA marker names a signer that is not a signer ref in the request. Fix the request and use a new key on the same upload.
  • marker_page_unsupportedA page with a marker cannot be converted exactly. Fix the PDF and upload it again, or send fields with a new key on the same upload.
  • marker_pdf_too_complexThe PDF is over 5 million characters, 200,000 text items, or 256 MB of decoded streams. Send a simpler PDF, or send fields with a new key on the same upload.

A refused send is saved against its Idempotency-Key, so retrying that key returns the same refusal. An uploaded PDF cannot be replaced, so a corrected PDF always needs a new upload. A temporarily_unavailable response is not saved: retry the same request with the same key. Error responses never include marker text, and param gives only the location, such as markers.page.3.

Webhooks

Webhooks tell your server as soon as something happens to a document, so you do not have to poll. Add up to 5 endpoints on API settings. Each URL must use HTTPS on port 443 with a hostname rather than an IP address, and can be up to 2,048 characters.

Events

  • document.sentA document went out for signature, from the API or from the app. A public signing link copy emits this only once its visitor signs, together with recipient.signed.
  • recipient.viewedA signer opened the document for the first time.
  • recipient.signedA signer finished signing. Other signers may still be outstanding.
  • recipient.declinedA signer declined, which ends the document. Fetch the document for the reason.
  • document.completedEveryone signed and the signed PDF is saved, so you can download it straight away.
  • document.voidedThe document was voided.
  • pingA test event from Send test event on an endpoint's page in API settings. It has no document.

Payload

Payloads are thin: enough to identify what changed, never field values, signing links, or file contents. Fetch the document with the API when you need more.

recipient.signed
{
  "type": "recipient.signed",
  "id": "7d6b1f0a-3c4e-4a55-9e21-0b8f6c2d1a90",
  "created_at": "2026-11-02T15:04:05.123Z",
  "schema_version": 1,
  "document": {
    "id": "3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34",
    "title": "Consulting agreement",
    "status": "sent",
    "external_id": "crm-8841"
  },
  "recipient": {
    "id": "5b7e9d13-2c84-4f06-b1a9-6d40e8c2f715",
    "role": "signer",
    "name": "Alex Morgan",
    "email": "alex@example.com"
  }
}
Payload fields
  • idUUIDRequired

    The event ID. Equals the webhook-id header and stays the same on every retry, so dedupe on it.

  • created_attimestampRequired

    When the event happened, in UTC.

  • schema_version1Required
  • type"recipient.signed"Required
  • documentobjectRequired
    4 child fields
    • idUUIDRequired
    • titlestringRequired
    • statusstringRequired

      The document's status when the event fired.

    • external_idstring or nullRequired
  • recipientobjectRequired
    4 child fields
    • idUUIDRequired
    • rolestringRequired

      One of signer, cc

    • namestring or nullRequired
    • emailstring or nullRequired

recipient appears only on recipient.* events.

Verifying signatures

Inkly signs every delivery in the Standard Webhooks format, so any library for that format works too. Each request has three headers: webhook-id, webhook-timestamp, and webhook-signature.

The signature is v1, followed by the base64 HMAC-SHA256 of id.timestamp.body, using the base64-decoded part of your secret after whsec_ as the key.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto"

const MAX_AGE_SECONDS = 5 * 60

// Pass the raw request body as a string, before any JSON parsing.
export function verifyInklyWebhook(rawBody, headers, secret) {
  const id = headers["webhook-id"]
  const timestamp = headers["webhook-timestamp"]
  const signatures = headers["webhook-signature"]
  if (!id || !timestamp || !signatures) return false

  const age = Math.abs(Date.now() / 1000 - Number(timestamp))
  if (!(age <= MAX_AGE_SECONDS)) return false

  const key = Buffer.from(secret.slice("whsec_".length), "base64")
  const expected = Buffer.from(
    "v1," + createHmac("sha256", key).update(id + "." + timestamp + "." + rawBody).digest("base64"),
  )

  // During a secret roll the header carries two signatures separated by a space.
  return signatures.split(" ").some((signature) => {
    const candidate = Buffer.from(signature)
    return candidate.length === expected.length && timingSafeEqual(candidate, expected)
  })
}
Verify against the raw body, exactly as received. Parsing and re-encoding the JSON changes the bytes and the signature will not match.

Retries and timing

Any 2xx response within 10 seconds counts as delivered. Redirects are not followed. After a failure, Inkly retries a document or recipient event after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 24 hours, and 24 hours: 8 retries over about 3 days. Send test event makes one attempt, and Resend makes one more. Neither retries if it fails.

An endpoint that has been failing for more than 3 days, with at least 5 failed attempts, is turned off automatically and the people who manage keys get one email. API settings shows each endpoint's deliveries for 30 days, with Resend and Send test event. Changing an endpoint's URL cancels its pending deliveries.

Handling duplicates and order

Delivery is at least once and order is not guaranteed, so duplicates are normal and recipient.signed can arrive after document.completed. Store each webhook-id you have processed and skip repeats. It equals the payload's id and stays the same on every retry, while webhook-timestamp changes.

Rolling a secret

When you roll a secret, the old one keeps signing for 24 hours. During that time webhook-signature holds two signatures separated by a space, and a delivery is valid if either matches, so you can update your server without dropping events.