Skip to main content
After the user signs, your backend creates the final PDF using the SuperDoc API.

Flow

Frontend sends → Backend processes → PDF created
    ↓                   ↓                 ↓
Signing data    1. Annotate fields    Stored
                2. Apply signature

Complete example

app.post('/api/sign', async (req, res) => {
  try {
    const { eventId, document, auditTrail, documentFields, signerFields, signer } = req.body;

    if (!document?.url) {
      return res.status(400).json({ error: 'document.url is required' });
    }
    
    // 1. Fill document fields
    const annotated = await fetch('https://api.superdoc.dev/v1/annotate?to=pdf', {
      method: 'POST',
      headers: { 
        'Authorization': `Bearer ${process.env.SUPERDOC_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        document: { url: document.url },
        fields: [...documentFields, ...signerFields]
      })
    });
    
    if (!annotated.ok) throw new Error('Annotation failed');
    
    // 2. Apply digital signature
    const annotatedData = await annotated.json();
    const signed = await fetch('https://api.superdoc.dev/v1/sign', {
      method: 'POST', 
      headers: { 
        'Authorization': `Bearer ${process.env.SUPERDOC_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        eventId,
        document: { base64: annotatedData?.document?.base64 },
        auditTrail,
        signer
      })
    });
    
    if (!signed.ok) throw new Error('Signing failed');
    
    // 3. Save signed PDF
    const signedData = await signed.json();
    const signedPdfBase64 = signedData?.document?.base64;
    await saveToS3(Buffer.from(signedPdfBase64, 'base64'), `signed/${eventId}.pdf`);

    // 4. Log for compliance
    await db.signatures.create({
      eventId,
      signerName: signerFields.find(f => f.id === '1')?.value,
      signedAt: new Date(),
      auditTrail: JSON.stringify(auditTrail)
    });
    
    res.json({ success: true, documentId: eventId });
    
  } catch (error) {
    console.error('Signing failed:', error);
    res.status(500).json({ error: 'Failed to sign document' });
  }
});

Download/PDF generation

Handle download requests to generate PDFs on-demand:
app.post('/api/generate-pdf', async (req, res) => {
  const { documentSource, fields } = req.body;

  if (!documentSource || typeof documentSource !== 'string') {
    return res.status(400).json({ error: 'documentSource URL is required' });
  }

  const result = await fetch('https://api.superdoc.dev/v1/annotate?to=pdf', {
    method: 'POST',
    headers: { 
      'Authorization': `Bearer ${process.env.SUPERDOC_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      document: { url: documentSource },
      fields: [...(fields?.document || []), ...(fields?.signer || [])]
    })
  });
  
  if (!result.ok) throw new Error('PDF generation failed');
  
  // Send PDF back to frontend
  const data = await result.json();
  const pdfBase64 = data?.document?.base64;
  res.type('application/pdf');
  res.send(Buffer.from(pdfBase64, 'base64'));
});

Data structure

The frontend sends the onSubmit payload plus a document reference and signer details your backend can access:
{
  "eventId": "session-123",
  "document": { "url": "https://example.com/agreement.docx" },
  "signer": {
    "name": "Jane Smith",
    "email": "jane@example.com"
  },
  "documentFields": [
    { "id": "1", "value": "Jane Smith" }
  ],
  "signerFields": [
    { "id": "1", "value": "Jane Smith" },
    { "id": "2", "value": true }
  ],
  "auditTrail": [
    { "type": "ready", "timestamp": "2024-01-15T10:30:00Z" },
    { "type": "scroll", "timestamp": "2024-01-15T10:30:15Z", 
      "data": { "percent": 100 } },
    { "type": "field_change", "timestamp": "2024-01-15T10:30:30Z",
      "data": { "fieldId": "1", "value": "Jane Smith" } },
    { "type": "submit", "timestamp": "2024-01-15T10:30:45Z" }
  ],
  "timestamp": "2024-01-15T10:30:45Z",
  "duration": 45,
  "isFullyCompleted": true
}

Signer fields

When forwarding this payload to POST /v1/sign, only the following signer fields are accepted:
FieldRequiredDescription
nameyesSigner’s full name (2–255 chars). Rendered on the signature.
emailyesSigner’s email address (valid email, max 255 chars).
ipnoIPv4 address of the signer. Included in the audit trail certificate.
userAgentnoBrowser user agent string. Included in the audit trail certificate.
No other properties are accepted on signer — the request is rejected with a validation error if extra keys are sent. For application-specific context (tenantId, contractId, etc.), pass a top-level metadata object instead.
Do not trust ip or userAgent values from the browser-submitted payload. Derive them server-side from the incoming request (e.g. req.ip, req.headers['user-agent']) before forwarding to /v1/sign, so the audit trail certificate reflects what your server actually observed.

API reference