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