Guides & Tutorials

Send Signers to a Localized Signing Experience With One URL Parameter

Alt text: "Dark-themed graphic with 'Localized Signing Experience' in white text. Multi-language text and a digital document with colorful interface elements are shown, conveying a professional and modern tone."

If you're sending contracts to signers in multiple countries, here's the fastest path to a localized experience for each of them. Append ?lang=fr to a French signer's link, ?lang=de to a German signer's link, and Firma.dev handles the rest. Every screen they see, from instructions and buttons through to legal text, loads in that language from the first page. No account required on their side, no settings to configure on yours.

How the ?lang= Parameter Works

The ?lang= parameter can be appended to any Firma.dev signing link. It forces the entire signing experience into the specified language, overriding both automatic browser detection and any workspace-level defaults. The signer doesn't need to do anything, they just follow the link and the interface is already in the right language.

The seven supported language codes are:

Code

Language

en

English

es

Spanish

fr

French

de

German

it

Italian

pt

Portuguese

el

Greek

A standard signing URL looks like this:

https://app.firma.dev/signing/{signing_request_user_id}
https://app.firma.dev/signing/{signing_request_user_id}
https://app.firma.dev/signing/{signing_request_user_id}

With a language override:

https://app.firma.dev/signing/{signing_request_user_id}?lang=fr
https://app.firma.dev/signing/{signing_request_user_id}?lang=fr
https://app.firma.dev/signing/{signing_request_user_id}?lang=fr

That's the entire change. No additional API calls, no template modifications, no per-signer configuration.

One Signing Request, Multiple Localized Links

The practical pattern for multi-party or international documents: create a single signing request with Firma.dev's automated emails disabled, fetch the signing URLs for each recipient, then append the appropriate ?lang= parameter for each one before sending through your own system.

The following code is from the white-labeling guide, with ?lang= appended per the localization guide:

// Create signing request with emails disabled
const response = await fetch(
  'https://api.firma.dev/functions/v1/signing-request-api/signing-requests',
  {
    method: 'POST',
    headers: {
      'Authorization': API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      workspace_id: workspaceId,
      template_id: templateId,
      settings: {
        send_signing_email: false,
        send_finish_email: false
      },
      recipients: [
        { first_name: 'Jane', last_name: 'Smith', email: 'jane@example.com', designation: 'Signer', order: 1 }
      ]
    })
  }
)
const signingRequest = await response.json()

// Get signing URLs for each recipient
const usersResponse = await fetch(
  `https://api.firma.dev/functions/v1/signing-request-api/signing-requests/${signingRequest.id}/users`,
  {
    headers: { 'Authorization': API_KEY }
  }
)
const { results: users } = await usersResponse.json()

// Send emails through your own system with ?lang= appended
for (const user of users) {
  const signingUrl = `https://app.firma.dev/signing/${user.id}?lang=fr`
  await yourEmailService.send({
    to: user.email,
    subject: 'Please sign your document',
    body: `Click here to sign: ${signingUrl}`
  })
}
// Create signing request with emails disabled
const response = await fetch(
  'https://api.firma.dev/functions/v1/signing-request-api/signing-requests',
  {
    method: 'POST',
    headers: {
      'Authorization': API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      workspace_id: workspaceId,
      template_id: templateId,
      settings: {
        send_signing_email: false,
        send_finish_email: false
      },
      recipients: [
        { first_name: 'Jane', last_name: 'Smith', email: 'jane@example.com', designation: 'Signer', order: 1 }
      ]
    })
  }
)
const signingRequest = await response.json()

// Get signing URLs for each recipient
const usersResponse = await fetch(
  `https://api.firma.dev/functions/v1/signing-request-api/signing-requests/${signingRequest.id}/users`,
  {
    headers: { 'Authorization': API_KEY }
  }
)
const { results: users } = await usersResponse.json()

// Send emails through your own system with ?lang= appended
for (const user of users) {
  const signingUrl = `https://app.firma.dev/signing/${user.id}?lang=fr`
  await yourEmailService.send({
    to: user.email,
    subject: 'Please sign your document',
    body: `Click here to sign: ${signingUrl}`
  })
}
// Create signing request with emails disabled
const response = await fetch(
  'https://api.firma.dev/functions/v1/signing-request-api/signing-requests',
  {
    method: 'POST',
    headers: {
      'Authorization': API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      workspace_id: workspaceId,
      template_id: templateId,
      settings: {
        send_signing_email: false,
        send_finish_email: false
      },
      recipients: [
        { first_name: 'Jane', last_name: 'Smith', email: 'jane@example.com', designation: 'Signer', order: 1 }
      ]
    })
  }
)
const signingRequest = await response.json()

// Get signing URLs for each recipient
const usersResponse = await fetch(
  `https://api.firma.dev/functions/v1/signing-request-api/signing-requests/${signingRequest.id}/users`,
  {
    headers: { 'Authorization': API_KEY }
  }
)
const { results: users } = await usersResponse.json()

// Send emails through your own system with ?lang= appended
for (const user of users) {
  const signingUrl = `https://app.firma.dev/signing/${user.id}?lang=fr`
  await yourEmailService.send({
    to: user.email,
    subject: 'Please sign your document',
    body: `Click here to sign: ${signingUrl}`
  })
}

The document is the same for everyone. The signing experience each person sees is in their language. You handle the link construction, Firma.dev handles the rendering. Swap ?lang=fr for whichever language code matches each recipient.

This pattern is particularly useful when Firma.dev's automated emails are disabled and you're distributing signing links through your own notification system. You already control the delivery, so adding the ?lang= parameter is a trivial addition.

Automatic Fallback Without the Parameter

When no ?lang= parameter is present, Firma.dev reads the signer's browser language settings and matches to the closest supported language. If their browser is set to French, they'll see a French interface. If their language isn't supported, it defaults to English.

This fallback works well for direct links where you don't control the delivery context. But for embedded signing in iframes, or any scenario where you're constructing URLs server-side, passing ?lang= explicitly is the more reliable approach. Browser language detection inside an iframe can be inconsistent depending on how the parent application is configured.

The docs recommendation is straightforward: use ?lang= in embedded contexts and when you know the signer's locale, let browser detection handle it for direct links where you don't.

The Complete Localization Picture

The ?lang= parameter is one piece of Firma.dev's localization stack. For a fully localized signing workflow, the pieces fit together like this:

Signing experience language is controlled by ?lang= on the link, or by browser detection as a fallback. This covers everything the signer sees during the signing flow.

Notification emails are controlled by the language field on workspace settings. Set the workspace to French and all automated signing emails go out in French, with no custom template required.

Signature styles are handled automatically based on each signer's name. Greek, Cyrillic, Japanese, and Korean names get signature fonts built for their writing system without any configuration.

Together these three layers cover the full experience: the emails going in, the signing flow itself, and how the signature looks on the document. Firma.dev's localization guide covers all three in detail.

Get your API key and start building for free, no credit card required.

  1. Heading

Background Image

Ready to add e-signatures to your application?

Get started for free. No credit card required. Pay only €0.029 per envelope when you're ready to go live.

Background Image

Ready to add e-signatures to your application?

Get started for free. No credit card required. Pay only €0.029 per envelope when you're ready to go live.

Background Image

Ready to add e-signatures to your application?

Get started for free. No credit card required. Pay only €0.029 per envelope when you're ready to go live.