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.
Signing sessions
Section titled “Signing sessions”Email delivery creates sessions for you (the link in the email). For embedded or self-delivered signing, mint one from your backend:
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.
What the signer goes through
Section titled “What the signer goes through”The signing page (and any custom UI you build on the same API) follows these steps:
- 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. - Load:
GET /v1/signing/sessionreturns the signer view (envelope title, sender, the recipient’s role and fields, prefilled values, the consumer disclosure) and recordsrecipient.viewedthe first time.GET /v1/signing/documentis the PDF to display. - Verify (only with
email_otp):POST /v1/signing/otpemails a six-digit code (10 minutes) andPOST /v1/signing/otp/verifychecks it. A wrong code returnsverified: falsewith the attempts left. The view’sverification.verifiedis alreadytruewhen the recipient’s method needs no code. - Submit:
POST /v1/signing/submitwith the fieldvalues(signatures drawn, typed or uploaded), consent to the disclosure, andviewedDocumentHash: the SHA-256 of the exact PDF that was shown. If the document changed since, the submit is refused (409). - 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.
Signer verification
Section titled “Signer verification”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 useintegrator_asserted(withexternalUserId) oremail_otpfor 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.
Languages
Section titled “Languages”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:
- the language they picked from the page’s language menu (remembered on that device);
<bf-signing locale="…">(embedded signing) or?locale=on a hosted link;- the recipient’s
locale, set when the envelope is created (recipients[].locale, orlocaleon the envelope for everyone without their own); - 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 |
Always-on signing links
Section titled “Always-on signing links”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.
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"] } }'visitorRoleis 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 givenemail. 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, thenPOST /v1/links/{id}/verify). limitscaps use per email per day (default 3), per IP per hour (default 20), per day overall, and by email domain. Withdedupe(default on), an email address that already signed gets409 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 emitssigning_link.used.