Signing links

Guides

Signing links

Let people sign from inside your own product instead of from Inkly's email. Your app asks the API for a signer's link and sends them to it, from a Sign now button, a chat message, or an SMS.

Every signer has a delivery, which you choose when you send:

  • email, the default. Inkly emails the invitation, and reminders if you turn them on.
  • link. Inkly sends no invitation and no reminders, and you share the signing link yourself. The signer still needs an email address, because everyone gets a copy of the completed document.

You can mix the two on one document, and both template sends and PDF sends accept it. CC recipients do not sign, so they have no link and always get email.

link is not a test mode. The document is real and legally binding, counts toward your allowance, and sends completion copies.

Set delivery on the signer:

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",
    "recipients": [
      {
        "role_id": "c81e7f42-0b39-4d6a-a7c4-92e15d3f8b76",
        "name": "Alex Morgan",
        "email": "alex@example.com",
        "delivery": "link"
      }
    ]
  }'

The response lists each recipient with a signing_url. It is set for a link signer whose turn it is, and null for everyone else:

The response's recipients
{
  "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
    }
  ]
}

Store the recipient's id next to the person in your own records. You need it, with the document ID, to get their link again later.

Ask for a signer's link at any time while the document is out for signature:

curl -X POST "https://www.inkly.so/api/v1/documents/3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34/recipients/5b7e9d13-2c84-4f06-b1a9-6d40e8c2f715/signing-link" \
  -H "Authorization: Bearer $INKLY_API_KEY"
The response
{
  "signing_url": "https://www.inkly.so/documents/3f1c2a7e-8b4d-4e21-9a6f-5d0c7e1b2a34/sign/example-signing-token"
}

This works for email signers too, so someone you invited by email can also sign from your app. Each request is recorded in the document's audit trail and on its Certificate of Completion.

When a link is available

A link is available only for a signer who can sign right now. In every other case the request returns 409 with signing_link_unavailable, which means one of these:

  • The document is no longer out for signature, because it was completed, declined, or voided.
  • The person has already signed or declined.
  • The recipient is a CC, or signs in person.
  • The document uses sequential signing, and it is not this signer's turn yet.

With sequential signing, get each signer's link when their turn comes:

  1. Listen for recipient.signed

    A webhook tells you each time someone signs.

  2. Find who is next

    Get the document and look for the recipient whose is_signing_turn is true.

  3. Get their link and tell them

    Request their link, or wait until they open your app and get it then.

Send people to sign from your app

The safest pattern is a route in your own app that the signer opens while signed in to your app. It checks who they are, gets their link, and redirects them to it, so the link is never stored, emailed, or shown on a page:

import express from "express"

const app = express()

// requireUser is your own sign-in check. Never trust a recipient ID that comes from the browser.
app.get("/agreements/:id/sign", requireUser, async (req, res) => {
  const signer = await findSignerForUser(req.user, req.params.id)
  if (!signer) return res.sendStatus(404)

  const response = await fetch(
    `https://www.inkly.so/api/v1/documents/${signer.documentId}/recipients/${signer.recipientId}/signing-link`,
    { method: "POST", headers: { Authorization: `Bearer ${process.env.INKLY_API_KEY}` } },
  )
  // 409 means they have already signed, or it is not their turn yet.
  if (response.status === 409) return res.render("not-ready")
  if (!response.ok) throw new Error(`Inkly API ${response.status}: ${await response.text()}`)

  const { signing_url } = await response.json()
  // Ask for the link each time rather than storing it, and keep it out of logs.
  res.set("Cache-Control", "no-store").redirect(303, signing_url)
})

findSignerForUser stands for your own lookup from a signed-in user to the document and recipient IDs you stored when you sent. The signer lands on Inkly's signing page in the same tab. It cannot be shown inside an iframe, because Inkly's pages refuse to be framed, and Inkly does not send the signer back to your app afterwards. Listen for recipient.signed to update your app instead.

How long a link works

A signing link has no expiry time of its own, and asking again returns the same link. It keeps working until the signer signs or declines, or the document is completed, declined, or voided. To stop a link working before then, void the document.

Rate limit

A workspace can get 300 signing links an hour, across all its keys. Past that, the request returns 429 with rate_limited and a Retry-After header. Ask for a link when someone is about to use it, rather than for every signer in advance. The limits page has every other limit.

Anyone holding a signing link can sign as that person, so treat it like a password:

  • Give it only to the person it belongs to, after your app has checked who they are.
  • Never write it to logs, error reports, or analytics, and never put it on a public page.
  • Get it when it is needed instead of storing it. If you must store it, protect it like a password.
  • Get it from your server. Your API key must never reach a browser.