Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.firma.dev/llms.txt

Use this file to discover all available pages before exploring further.

Embeddable signing

Embed the signing flow directly in your application so recipients can sign documents without leaving your product. This creates a seamless, white-label signing experience.

Signing URL pattern

The signing interface is available at: https://app.firma.dev/signing/{signing_request_user_id}

Basic iframe embed

<iframe 
  src="https://app.firma.dev/signing/{signing_request_user_id}" 
  style="width:100%;height:900px;border:0;" 
  allow="camera;microphone;clipboard-write"
  title="Document Signing"
></iframe>

Required iframe permissions

  • camera - For identity verification (if enabled)
  • microphone - For video verification (if enabled)
  • clipboard-write - For copying/pasting content

Getting the signing URL

The signing_request_user_id is returned when you fetch signing request users via the API.

Example: Get recipient signing URLs

const response = await fetch(
  `https://api.firma.dev/functions/v1/signing-request-api/signing-requests/${signingRequestId}/users`,
  {
    headers: {
      'Authorization': `Bearer ${API_KEY}`
    }
  }
)

const users = await response.json()

// Each user has their own signing URL
users.forEach(user => {
  const signingUrl = `https://app.firma.dev/signing/${user.id}`
  console.log(`${user.email}: ${signingUrl}`)
})

Complete implementation example

React component

import { useState, useEffect } from 'react'

function SigningView({ signingRequestId, recipientEmail }) {
  const [signingUrl, setSigningUrl] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    async function loadSigningUrl() {
      try {
        // Fetch signing request users from your backend
        const response = await fetch(
          `/api/signing-requests/${signingRequestId}/users`,
          { credentials: 'include' }
        )
        
        if (!response.ok) {
          throw new Error('Failed to load signing users')
        }
        
        const users = await response.json()
        
        // Find user by email
        const user = users.find(
          u => u.email === recipientEmail
        )
        
        if (!user) {
          throw new Error('User not found')
        }
        
        const url = `https://app.firma.dev/signing/${user.id}`
        setSigningUrl(url)
        setLoading(false)
      } catch (err) {
        setError(err.message)
        setLoading(false)
      }
    }
    
    loadSigningUrl()
  }, [signingRequestId, recipientEmail])
  
  if (loading) return <div>Loading signing interface...</div>
  if (error) return <div>Error: {error}</div>
  
  return (
    <iframe
      src={signingUrl}
      style={{
        width: '100%',
        height: '900px',
        border: 0
      }}
      allow="camera;microphone;clipboard-write"
      title="Sign Document"
    />
  )
}

Server-side endpoint (Node.js)

import express from 'express'
import fetch from 'node-fetch'

const app = express()
const API_KEY = process.env.FIRMA_API_KEY

app.get('/api/signing-requests/:id/users', async (req, res) => {
  try {
    const response = await fetch(
      `https://api.firma.dev/functions/v1/signing-request-api/signing-requests/${req.params.id}/users`,
      {
        headers: {
          'Authorization': `Bearer ${API_KEY}`
        }
      }
    )
    
    if (!response.ok) {
      const error = await response.text()
      return res.status(response.status).json({ error })
    }
    
    const users = await response.json()
    res.json(users)
  } catch (error) {
    console.error('Failed to fetch signing users:', error)
    res.status(500).json({ error: 'Internal server error' })
  }
})

app.listen(3000)

postMessage events

The signing iframe emits postMessage events for tracking signing progress:
window.addEventListener('message', (event) => {
  // Validate origin
  if (event.origin !== 'https://app.firma.dev') return
  
  const data = event.data
  
  switch (data.type) {
    case 'signing.started':
      console.log('User started signing')
      break
    case 'signing.completed':
      console.log('Document signed successfully')
      // Redirect or show success message
      break
    case 'signing.declined':
      console.log('User declined to sign')
      break
    case 'signing.error':
      console.error('Signing error:', data.error)
      break
  }
})

Security best practices

Never expose API keys in frontend code. Always fetch signing user IDs through a secure backend endpoint.

✅ Do’s

  • ✅ Fetch signing user IDs through your backend
  • ✅ Validate postMessage origins (https://app.firma.dev)
  • ✅ Use HTTPS for all API requests
  • ✅ Monitor signing events via webhooks (60 req/min)

❌ Don’ts

  • ❌ Don’t expose API keys in client code
  • ❌ Don’t trust postMessage data without origin validation

Rate Limits

See the guide on Rate Limits.

Webhook integration

Use webhooks to track signing events in real-time instead of polling:
// Webhook handler
app.post('/webhooks/firma', async (req, res) => {
  const event = req.body
  
  switch (event.event_type) {
    case 'signing_request.viewed':
      await notifyUser(event.data.signing_request_id, 'viewed')
      break
    case 'signing_request.recipient.signed':
      await notifyUser(event.data.signing_request_id, 'signed')
      break
    case 'signing_request.completed':
      await markComplete(event.data.signing_request_id)
      break
  }
  
  res.json({ received: true })
})
See the Webhooks guide for complete implementation details.

Troubleshooting

Iframe not loading

Possible causes:
  • Invalid signing_request_user_id
  • Recipient already completed signing
  • Signing request was cancelled or expired
Solution: Verify signing request status via API

postMessage events not received

Possible causes:
  • Origin validation blocking messages
  • Event listener not attached before iframe loads
Solution:
  • Check origin is exactly https://app.firma.dev
  • Attach listener before creating iframe

403 Forbidden when fetching signing request

Possible causes:
  • User doesn’t have access to the signing request
  • API key lacks required permissions
Solution: Verify authorization logic and API key permissions

Next steps