> ## 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 experience into your product so signers can complete documents without leaving your app.

# 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

```html theme={null}
<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

```js theme={null}
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

```jsx theme={null}
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)

```js theme={null}
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:

```js theme={null}
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

<Warning>
  **Never expose API keys in frontend code.** Always fetch signing user IDs through a secure backend endpoint.
</Warning>

### ✅ 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](/guides/rate-limits).

## Webhook integration

Use webhooks to track signing events in real-time instead of polling:

```js theme={null}
// 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](/guides/webhooks) 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

* [Send signing requests](/guides/sending-signing-request) with custom templates
* [Set up webhooks](/guides/webhooks) for real-time event notifications (60 req/min)
* [Workspace settings](/guides/workspace-settings) for customizing email branding (100-200 req/min)
