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.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