Guides

Auto-Place Signature Fields with Anchor Tags: Text-Based Field Positioning

Abstract graphic with dark background features a split rectangle, dotted lines labeled "X=?" and "Y=?", and an arrow pointing right, suggesting transformation.

If you've ever positioned signature fields by calculating x/y coordinates on a PDF, you know how tedious it gets at scale. Change the document layout, and every coordinate breaks. Add a second page, and you're recalculating offsets for half your fields.

Anchor tags fix this. Embed text markers like {{SIGN_HERE}} or {{DATE}} directly in your PDF template, and Firma.dev automatically detects them and places the correct field types at those locations. The markers get stripped from the final document, so signers never see them. One API call, no coordinate math.

Anchor tags shipped in v1.11.0 and support all field types, flexible matching rules, and up to 100 tags per signing request.

Quick Start

The fastest way to get anchor tags working: embed a marker in your PDF, then create a signing request with an anchor_tags array.

Step 1: Add the text {{SIGN_HERE}} somewhere in your PDF where you want the signature field to appear. You can do this in whatever tool generates your documents, whether that's a Word template, a PDF library, or a document generation service.

Step 2: Create a document-based signing request with the anchor tag definition:

const response = await fetch(
  'https://api.firma.dev/functions/v1/signing-request-api/signing-requests',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FIRMA_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      workspace_id: workspaceId,
      document: documentBase64,
      name: 'NDA - Acme Corp',
      anchor_tags: [
        {
          anchor_string: '{{SIGN_HERE}}',
          type: 'signature',
          recipient_id: 'temp_1',
          required: true
        }
      ],
      recipients: [
        {
          id: 'temp_1',
          first_name: 'Jane',
          last_name: 'Smith',
          email: 'jane@example.com',
          designation: 'Signer',
          order: 1
        }
      ]
    })
  }
);

const signingRequest = await response.json();
const response = await fetch(
  'https://api.firma.dev/functions/v1/signing-request-api/signing-requests',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FIRMA_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      workspace_id: workspaceId,
      document: documentBase64,
      name: 'NDA - Acme Corp',
      anchor_tags: [
        {
          anchor_string: '{{SIGN_HERE}}',
          type: 'signature',
          recipient_id: 'temp_1',
          required: true
        }
      ],
      recipients: [
        {
          id: 'temp_1',
          first_name: 'Jane',
          last_name: 'Smith',
          email: 'jane@example.com',
          designation: 'Signer',
          order: 1
        }
      ]
    })
  }
);

const signingRequest = await response.json();
const response = await fetch(
  'https://api.firma.dev/functions/v1/signing-request-api/signing-requests',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FIRMA_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      workspace_id: workspaceId,
      document: documentBase64,
      name: 'NDA - Acme Corp',
      anchor_tags: [
        {
          anchor_string: '{{SIGN_HERE}}',
          type: 'signature',
          recipient_id: 'temp_1',
          required: true
        }
      ],
      recipients: [
        {
          id: 'temp_1',
          first_name: 'Jane',
          last_name: 'Smith',
          email: 'jane@example.com',
          designation: 'Signer',
          order: 1
        }
      ]
    })
  }
);

const signingRequest = await response.json();

Firma.dev scans the PDF for {{SIGN_HERE}}, places a signature field at that location, assigns it to the recipient, and removes the marker text from the document. That's it.

Note that anchor tags only work with document-based signing request creation, not template-based. You're sending the raw PDF and letting the anchor system handle field placement.

Supported Field Types

Anchor tags support every field type available in Firma.dev. Set the type property on each anchor tag to one of these values:

signature for signature fields, initials for initial fields, text for single-line text input, textarea for multi-line text input, date for date fields, checkbox for checkboxes, radio for radio button groups, dropdown for dropdown selects, url for URL fields, stamp for stamp fields, and file for file upload fields.

Each type renders the appropriate field widget in the signing experience. A date anchor tag creates a date picker, a dropdown creates a select menu, and so on.

Matching and Positioning Options

The default matching behavior works for most cases, but you can fine-tune how Firma.dev finds and positions anchor-created fields.

Case sensitivity: Set case_sensitive to true or false (default is false). With case-insensitive matching, {{sign_here}} and {{SIGN_HERE}} both match.

Whole word matching: Set whole_word to true to prevent partial matches. If your anchor string is SIGN and whole_word is false, it would also match SIGNATURE or COSIGN. Setting it to true ensures it only matches the exact word.

Targeting specific occurrences: If your PDF contains the same anchor string multiple times, you can target specific ones with occurrence. Set it to 1 for the first match, 2 for the second, and so on. Omit it or set match_all to true to place fields at every occurrence.

Offset positioning: Fine-tune where the field lands relative to the anchor text using offset_x and offset_y. You can specify offsets in percentages (relative to page dimensions) or pixels. This is useful when you want the field positioned slightly below or to the right of the marker, rather than directly on top of it.

Graceful handling of missing anchors: Set ignore_if_not_present to true if the anchor string might not exist in every document. Without this, a missing anchor returns an error. With it enabled, Firma.dev silently skips that tag and processes the rest.

Advanced Configuration

Here's a more complete example that combines multiple anchor tags with different field types, custom dimensions, and positioning offsets:

const response = await fetch(
  'https://api.firma.dev/functions/v1/signing-request-api/signing-requests',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FIRMA_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      workspace_id: workspaceId,
      document: documentBase64,
      name: 'Employment Agreement',
      anchor_tags: [
        {
          anchor_string: '{{EMPLOYEE_SIGNATURE}}',
          type: 'signature',
          recipient_id: 'temp_1',
          required: true,
          width: 30,
          height: 8,
          offset_y: 2
        },
        {
          anchor_string: '{{EMPLOYEE_DATE}}',
          type: 'date',
          recipient_id: 'temp_1',
          required: true
        },
        {
          anchor_string: '{{EMPLOYEE_INITIALS}}',
          type: 'initials',
          recipient_id: 'temp_1',
          required: true,
          match_all: true
        },
        {
          anchor_string: '{{MANAGER_SIGNATURE}}',
          type: 'signature',
          recipient_id: 'temp_2',
          required: true
        },
        {
          anchor_string: '{{BENEFITS_OPT_IN}}',
          type: 'checkbox',
          recipient_id: 'temp_1',
          required: false,
          ignore_if_not_present: true
        }
      ],
      recipients: [
        {
          id: 'temp_1',
          first_name: 'Sarah',
          last_name: 'Chen',
          email: 'sarah@example.com',
          designation: 'Signer',
          order: 1
        },
        {
          id: 'temp_2',
          first_name: 'Mike',
          last_name: 'Torres',
          email: 'mike@example.com',
          designation: 'Signer',
          order: 2
        }
      ]
    })
  }
);
const response = await fetch(
  'https://api.firma.dev/functions/v1/signing-request-api/signing-requests',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FIRMA_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      workspace_id: workspaceId,
      document: documentBase64,
      name: 'Employment Agreement',
      anchor_tags: [
        {
          anchor_string: '{{EMPLOYEE_SIGNATURE}}',
          type: 'signature',
          recipient_id: 'temp_1',
          required: true,
          width: 30,
          height: 8,
          offset_y: 2
        },
        {
          anchor_string: '{{EMPLOYEE_DATE}}',
          type: 'date',
          recipient_id: 'temp_1',
          required: true
        },
        {
          anchor_string: '{{EMPLOYEE_INITIALS}}',
          type: 'initials',
          recipient_id: 'temp_1',
          required: true,
          match_all: true
        },
        {
          anchor_string: '{{MANAGER_SIGNATURE}}',
          type: 'signature',
          recipient_id: 'temp_2',
          required: true
        },
        {
          anchor_string: '{{BENEFITS_OPT_IN}}',
          type: 'checkbox',
          recipient_id: 'temp_1',
          required: false,
          ignore_if_not_present: true
        }
      ],
      recipients: [
        {
          id: 'temp_1',
          first_name: 'Sarah',
          last_name: 'Chen',
          email: 'sarah@example.com',
          designation: 'Signer',
          order: 1
        },
        {
          id: 'temp_2',
          first_name: 'Mike',
          last_name: 'Torres',
          email: 'mike@example.com',
          designation: 'Signer',
          order: 2
        }
      ]
    })
  }
);
const response = await fetch(
  'https://api.firma.dev/functions/v1/signing-request-api/signing-requests',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FIRMA_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      workspace_id: workspaceId,
      document: documentBase64,
      name: 'Employment Agreement',
      anchor_tags: [
        {
          anchor_string: '{{EMPLOYEE_SIGNATURE}}',
          type: 'signature',
          recipient_id: 'temp_1',
          required: true,
          width: 30,
          height: 8,
          offset_y: 2
        },
        {
          anchor_string: '{{EMPLOYEE_DATE}}',
          type: 'date',
          recipient_id: 'temp_1',
          required: true
        },
        {
          anchor_string: '{{EMPLOYEE_INITIALS}}',
          type: 'initials',
          recipient_id: 'temp_1',
          required: true,
          match_all: true
        },
        {
          anchor_string: '{{MANAGER_SIGNATURE}}',
          type: 'signature',
          recipient_id: 'temp_2',
          required: true
        },
        {
          anchor_string: '{{BENEFITS_OPT_IN}}',
          type: 'checkbox',
          recipient_id: 'temp_1',
          required: false,
          ignore_if_not_present: true
        }
      ],
      recipients: [
        {
          id: 'temp_1',
          first_name: 'Sarah',
          last_name: 'Chen',
          email: 'sarah@example.com',
          designation: 'Signer',
          order: 1
        },
        {
          id: 'temp_2',
          first_name: 'Mike',
          last_name: 'Torres',
          email: 'mike@example.com',
          designation: 'Signer',
          order: 2
        }
      ]
    })
  }
);

A few things to notice in this example. The {{EMPLOYEE_INITIALS}} tag uses match_all: true, so if the PDF has initials markers on pages 1, 5, and 12, all three get fields. The {{BENEFITS_OPT_IN}} checkbox uses ignore_if_not_present: true because not every version of the employment agreement includes that section. And the offset_y: 2 on the signature field nudges it down slightly from the anchor text position.

Dimensions (width and height) follow the same percentage-based coordinate system used by manually positioned fields. Anchor tags and manual fields work together, so you can use anchors for the bulk of your fields and still add manual coordinate-based fields in the same request for edge cases.

Migration from Other Platforms

If you're migrating from another e-signature provider that uses text-based field placement, the concepts map directly. The anchor string syntax and property names differ, but the underlying mechansim is the same: embed a marker, define the field type, let the API handle placement.

From DocuSign Auto-Place: DocuSign uses anchorString, anchorXOffset, anchorYOffset, and tab-specific types like signHereTabs. In Firma.dev, this becomes anchor_string, offset_x, offset_y, and the universal type field. DocuSign's anchorIgnoreIfNotPresent maps to ignore_if_not_present. DocuSign's anchorCaseSensitive maps to case_sensitive. The main structural difference is that Firma.dev uses a single anchor_tags array with a type property, rather than separate arrays for each tab type.

From Yousign Smart Anchors: Yousign's concept of "smart anchors" with field type detection translates directly. The matching and positioning options are comparable, though the property names use Firma.dev's snake_case convention.

The limit is 100 anchor tags per signing request, which covers most document workflows comfortably. If you're processing documents that genuinely need more than 100 fields, consider splitting across multiple signing requests or using a combination of anchor tags and template-based field definitions.

API Reference

The anchor_tags array is available on the document-based signing request creation endpoint (POST /signing-requests). Each item in the array is an AnchorTag object with roughly 25 configurable properties.

The key properties:

anchor_string (required) is the text marker to search for in the PDF. type (required) is the field type to create. recipient_id assigns the field to a specific recipient using their ID or temporary ID. required marks the field as mandatory or optional. offset_x and offset_y adjust field position relative to the anchor location. width and height set custom field dimensions. case_sensitive controls whether matching is case-sensitive. whole_word prevents partial string matches. occurrence targets a specific instance when the anchor appears multiple times. match_all places fields at every occurrence. ignore_if_not_present skips the tag without error if the anchor text isn't found in the document.

For the complete schema with all properties and their types, see the API changelog for v1.11.0.

Next Steps

Anchor tags pair well with programmatic document generation. If your app generates contracts, NDAs, or onboarding documents from templates, you can embed anchor strings during generation and let Firma.dev handle field placement automatically on every request.

Get started with Firma.dev for free, no credit card required.

  1. Heading

Background Image

Ready to add e-signatures to your application?

Get started for free. No credit card required. Pay only €0.029 per envelope when you're ready to go live.

Background Image

Ready to add e-signatures to your application?

Get started for free. No credit card required. Pay only €0.029 per envelope when you're ready to go live.

Background Image

Ready to add e-signatures to your application?

Get started for free. No credit card required. Pay only €0.029 per envelope when you're ready to go live.