Skip to content

Signing sessions & links

Recipients sign on Bona Fide’s signing page: either the hosted page at https://sign.bonafidesign.com/s/…, or the same page embedded in your product with <bf-signing>. Recipients do not need an account. What lets them in is a signing session.

Email delivery creates sessions for you (the link in the email). For embedded or self-delivered signing, mint one from your backend:

Terminal window
curl https://api.bonafidesign.com/v1/envelopes/env_8Gm2kQ4rT1/sessions \
-H "Authorization: Bearer $BONA_FIDE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "recipient": "rcp_5Jw0pLs9Xe", "mode": "embedded", "allowedOrigins": ["https://app.example.com"], "ttlSeconds": 600 }'
{
"id": "ses_2Hq8vNw3Kd",
"token": "bfs_…",
"url": "https://sign.bonafidesign.com/s/?embed=1#session=bfs_…",
"expiresAt": "2026-10-13T14:08:04.512Z"
}
Field Notes
recipient A recipient of the envelope who can still sign (not CC, not finished)
mode embedded (default) or hosted. Hosted urls are …/s/<token>, to redirect to or send yourself
allowedOrigins Up to 10 origins that may frame the embedded page
returnUrl Where the hosted page sends the signer when they are done
ttlSeconds 60 to 3600, default 600

Sessions are single-use: once the recipient submits, the session is spent, and a used or expired session answers 410 Gone. Mint a fresh one each time your page renders. Sessions can only be created while the envelope is sent, and voiding the envelope ends them all.

The signing page (and any custom UI you build on the same API) follows these steps:

  1. Exchange: POST /v1/signing/exchange { "token": "bfs_…" } returns a short-lived access token (bfa_…) kept in memory. No cookies are involved, so third-party cookie blocking does not matter in an iframe.
  2. Load: GET /v1/signing/session returns the signer view (envelope title, sender, the recipient’s role and fields, prefilled values, the consumer disclosure) and records recipient.viewed the first time. GET /v1/signing/document is the PDF to display.
  3. Verify (only with email_otp): POST /v1/signing/otp emails a six-digit code (10 minutes) and POST /v1/signing/otp/verify checks it. A wrong code returns verified: false with the attempts left. The view’s verification.verified is already true when the recipient’s method needs no code.
  4. Submit: POST /v1/signing/submit with the field values (signatures drawn, typed or uploaded), consent to the disclosure, and viewedDocumentHash: the SHA-256 of the exact PDF that was shown. If the document changed since, the submit is refused (409).
  5. Or decline: POST /v1/signing/decline { "reason": "…" } ends the envelope (envelope.declined).

It is the recipient’s turn only when every earlier routing group is done; before that the view has canSign: false, and a submit answers 409 not-your-turn.

Each recipient has a verification method: the role’s (set in the template designer’s roles panel) unless the envelope overrides it per recipient (the send screen, or recipients[].verification).

Method What the signer does Use it when
none (default) Opens the unique signing link and signs The link is emailed to them: only someone with access to that inbox has it
email_otp Opens the link, then enters a six-digit code emailed to the same address You want a second, fresh proof at the moment of signing (high-value contracts, shared inboxes)
integrator_asserted Signs in your product, which vouches for who they are (externalUserId) Embedded signing behind your own login

none is the default because the link Bona Fide emails is itself unique to the recipient and unguessable: opening it shows control of the email address, which is the attribution electronic signature laws rely on (ESIGN / UETA in the US, simple electronic signatures under eIDAS). Comparable products default to it too. Templates published with email_otp keep it, and an explicit email_otp keeps working everywhere.

Two flows never rely on a link alone:

  • Always-on signing links (/l/…): the visitor types their own address, so they always confirm an emailed code before an envelope is created, whatever the template says.
  • Sessions you mint (POST /v1/envelopes/{id}/sessions) carry no proof of the email address at all: that is your login’s job, so use integrator_asserted (with externalUserId) or email_otp for them.

The certificate of completion records, per signer, what was actually used:

Recorded as When
Email link (unique link sent to address) none, signed from the emailed link
Email link + one-time code (sent to address) email_otp, signed from the emailed link
Signing link + one-time code (sent to address) An always-on link visitor
One-time code (sent to address) email_otp in a session you minted
Signing session issued by the sender none in a session you minted
Integrator assertion integrator_asserted

The public verification summary shows the same labels without the address.

The signing page, its emails (request, reminders, codes, completed, voided), the always-on link page and the viewer status page are available in 50 languages. A signer sees them in this order of preference:

  1. the language they picked from the page’s language menu (remembered on that device);
  2. <bf-signing locale="…"> (embedded signing) or ?locale= on a hosted link;
  3. the recipient’s locale, set when the envelope is created (recipients[].locale, or locale on the envelope for everyone without their own);
  4. their browser’s language.

Emails need a language before anyone opens a page, so they use the recipient’s locale and are English without one. Always-on links send the code email in the language the visitor’s page shows.

{
"template": "tpl_…",
"locale": "es-419",
"recipients": [
{ "role": "client", "name": "Ana Souza", "email": "ana@example.com", "locale": "pt-BR" },
{ "role": "company", "name": "Lee Park", "email": "lee@example.com" }
]
}

Close tags are accepted and stored as the language they map to (de-AT → de, es-MX → es-419, zh-TW → zh-Hant); anything else is a validation error. Translated consent disclosures note that the English text applies, and the signer can switch to it; the language the disclosure was read in is recorded with the signature (consentLocale in the recipient.signed audit event).

Code Language Native name
ar Arabic العربية right-to-left
hy Armenian Հայերեն
bn Bengali বাংলা
bg Bulgarian Български
zh-Hans Chinese (Simplified) 简体中文
zh-Hant Chinese (Traditional) 繁體中文
hr Croatian Hrvatski
cs Czech Čeština
da Danish Dansk
nl Dutch Nederlands
en-GB English (UK) English (UK)
en-US English (US) English (US)
et Estonian Eesti
fil Filipino Filipino
fi Finnish Suomi
fr French Français
fr-CA French (Canada) Français (Canada)
de German Deutsch
el Greek Ελληνικά
he Hebrew עברית right-to-left
hi Hindi हिन्दी
hu Hungarian Magyar
id Indonesian Bahasa Indonesia
it Italian Italiano
ja Japanese 日本語
ko Korean 한국어
lv Latvian Latviešu
lt Lithuanian Lietuvių
ms Malay Bahasa Melayu
mr Marathi मराठी
nb Norwegian Norsk bokmål
fa Persian (Farsi) فارسی right-to-left
pl Polish Polski
pt-BR Portuguese (Brazil) Português (Brasil)
pt-PT Portuguese (Portugal) Português (Portugal)
ro Romanian Română
ru Russian Русский
sr-Latn Serbian (Latin) Srpski
sk Slovak Slovenčina
sl Slovenian Slovenščina
es-419 Spanish (Latin America) Español (Latinoamérica)
es-ES Spanish (Spain) Español (España)
sw Swahili Kiswahili
sv Swedish Svenska
te Telugu తెలుగు
th Thai ไทย
tr Turkish Türkçe
uk Ukrainian Українська
ur Urdu اردو right-to-left
vi Vietnamese Tiếng Việt

A signing link is a permanent URL for contracts that many people sign the same way: a waiver, a contractor agreement, terms for a pilot. Every visitor who completes it gets their own envelope.

Terminal window
curl https://api.bonafidesign.com/v1/signing-links \
-H "Authorization: Bearer $BONA_FIDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "tpl_…",
"visitorRole": "contractor",
"countersign": { "role": "company", "mode": "auto" },
"limits": { "perEmailPerDay": 3, "perIpPerHour": 20, "dailyCap": 200, "allowedEmailDomains": ["example.com"] }
}'
  • visitorRole is the signer role the visitor takes. Links support one visitor and at most one countersigner: mode: "auto" countersigns automatically with the Bona Fide platform seal, mode: "person" routes the envelope to the given email. Every other non-CC role would have no recipient, so such templates are refused.
  • The visitor enters their name, email and the template’s variables, then confirms an email code before anything is created (POST /v1/links/{id}/start, then POST /v1/links/{id}/verify).
  • limits caps use per email per day (default 3), per IP per hour (default 20), per day overall, and by email domain. With dedupe (default on), an email address that already signed gets 409 already-signed.
  • PATCH /v1/signing-links/{id} pauses (active: false), resumes or moves the link to a newer version; deleting a link keeps the envelopes it created. Each use emits signing_link.used.