Etch E-sign
The Anvil Etch E-sign API allows you to collect e-signatures from within your app. Send a signature packet including multiple PDFs, images, and other uploads to one or more signers. Templatize your common PDFs then fill them with your user's information before sending out the signature packet.
Anvil can handle the whole orchestration of notifying signers, or you can have full control to notify signers and generate your own signature links via embedded signing. Completions can be tracked with webhooks or email notifications.
Example app & live demo

See the live demo to try out Etch signatures.
Clone the example app repo to get a feel for using the E-signature API in your own Anvil account.
Authentication
First you will need an API key. You can find your API key on the Organization Settings -> API Settings page. We provide language-specific API clients that wrap authentication and make using our API easier.
For more information on generating an API key and handling API authentication, check out the API getting started article.
Postman collection
Quickly see the Etch E-sign APIs in action with our E-sign Postman collection.
Creating a signature packet
The createEtchPacket
GraphQL mutation allows you to create and send a signature Packet in a single API call. This is the most common, and often the only, signature-related API mutation you will need.
Signature requests can orchestrated via email notifications (the default), or optionally via an "embedded" page which you can send them to directly.
Adding PDFs to your packet
In order to request signatures you will need to provide a list of documents that need to be signed. These documents should be provided in an array under the `files` key in the mutation variables. These objects can be:
- references to existing PDF Templates that you have already set up.
- completely new documents that you can upload + configure in the mutation call.
- a mixture of the above.
Referencing an existing PDF template
If you'd like to include an existing PDF Template in your signature packet, you'll need the PDF templated EID and then can use the CastReference structure. You should have already configured this PDF Template with all signature-related and other fields. For more information about setting up a PDF template please see this article. An example:
{...files: [{id: 'anExistingPDFTemplate', // An ID of your choosing for referencing this file elsewhere in the APIcastEid: 'abcd1234' // Your PDF Template EID}]}
Uploading a new PDF
If you'd like to create a new PDF Template and include it in your signature packet, we can support that. At a minimum, the file
and fields
properties must be supplied.
Note: you can upload both PDFs and Word doc (docx
) files. docx
files will be converted to PDF on upload so they can be filled and signed.
We support 2 flavors of new document uploading which will be provided in the file
property of each upload:
- A Multipart Upload that allows you to send a binary file. Consider using our Anvil Node Client to simplify interacting with our API when binary uploads are involved.
- A Base64 Upload that allows you to send a file that you have in a
base64
-encoded string format.
See the Upload GraphQL type for more info.
When creating a new PDF Template that will be used for signatures in your packet, you should also provide a fields
array. This is where you specify the signature-related and other fields that are present in the PDF and give them each IDs that can be used to refer to them elsewhere in the Signature process. At the very least, each field object will need id
, type
, pageNum
and rect
properties. See the CastField reference for more details on these.
Here's a simple example of how an object in your files
array might look:
{...files: [{id: 'aNewFile',file: <Upload>,fields: [{id: 'signatureOne',type: 'signature',// What page is it on? This is the 1st pagepageNum: 0,// Where is the field and how big is it?rect: { x: 253.12, y: 121.12, width: 33.22, height: 27.22 }}]...}]}
A full list of properties can be found in the EtchUpload reference.
Generating a PDF from HTML and CSS
Anvil can generate PDFs from HTML and CSS to include in your signature packet.
You must provide a title
string, filename
, and a markup
object containing your HTML and CSS. The title
will be encoded into the PDF document and serve as the page header. Signature fields can also be specified under fields
, which accepts an array of CastFields.
{...files: [{id: 'htmlToPDF1',filename: 'html-to-pdf.pdf',title: 'Test HTML to PDF',markup: {html: '<h1>Hello World</h1>',css: 'h1 { color: red; }',},fields: [// You can also add signature fields to the PDF generated from HTML{id: 'signatureField',type: 'signature',rect: { x: 300, y: 300, height: 30, width: 200 },pageNum: 0,}],},]...}
A full list of properties can be found in the EtchFileGenerate reference.
Generating a PDF from markdown
Similar to generating PDFs from HTML and CSS, you can generate a PDF from markdown fields when creating a signature packet.
You must provide a title
string, filename
, and a fields
array. The title
will be encoded into the PDF document and serve as the page header. PDF content, fill fields, and signature fields will be specified under fields
, which accepts an array of CastFields and VerbatimFields.
The file object is structured similarly to when you use the PDF generation API.
{...files: [{id: 'generatedInvoice1',filename: 'pet-food-invoice.pdf',title: 'Pet Food Expenses',fontSize: 12,textColor: '#222222',fields: [{content: 'March 4th, 2024',fontSize: 10,textColor: '#616161',},{table: {rows: [['Description', 'Quantity', 'Price'],['3x Roof Shingles', '15', '$60.00'],['5x Hardwood Plywood', '10', '$300.00'],['80x Wood Screws', '80', '$45.00'],],},},{id: 'generatedInvoice1_fillField',type: 'shortText',rect: { x: 50, y: 300, height: 30, width: 300 },pageNum: 0,},{id: 'generatedInvoice1_signatureField',type: 'signature',rect: { x: 300, y: 300, height: 30, width: 200 },pageNum: 0,}],},]...}
A full list of properties can be found in the EtchFileGenerate reference.
Adding signers
Now that you have determined the documents and fields within those documents that require signing, it's time to specify who you would like to actually sign them. This is done by providing an array of Signer objects to the signers
key in the mutation variables.
id
Each of these Signer objects must contain an id
that uniquely identifies it for referencing by other parts of the E-signature process. This can be any string of your choosing unique within this document.
name
The first and last name of the signer e.g. 'Sally Jones'
.
email
The email address of the signer e.g. 'sally@jones.com'
. Even embedded
signers require a valid email address.
routingOrder
By default, the order in which signers will be requested to fill out their portion of the Packet will be their order in this signers
array, but you can also specify the 1-based order via the routingOrder
key in the Signer object.
signerType
By default, we will solicit signatures from signers via email with signerType: "email"
. Your signer will get an email from Anvil with a link to complete their part of the signature process.
You can embed the signature process into your app or website with signerType: "embedded"
. By setting the signerType
to "embedded"
, you will have full control over the sign link. The signer will not be sent an email and it will be up to you to get the signer to complete their signatures via our embedded page. See the embedded signing section of this guide for more info.
Anvil also supports signerType: "in-person"
for signers you know will be signing in person. Please contact
sales for more information on in-person signing.
signatureMode
You can choose draw
or text
signatures. In draw mode, a signer will need to draw their signature and initials. In text
mode, they will be prompted to accept their signature. The default signatureMode
is draw
.
acceptEachField
When set to true
, the signer will be shown all of the packet's PDFs on the signing page. They will be required to click through each signature or initials box. When set to false
, the signer will be shown a list of documents they will be signing with an option to open any documents in the packet. The default is true
.
enableEmails
You can indicate which emails you would like the user to receive from anvil. You may want to turn them all off to control the process yourself. Or leave them on to save yourself some work!
{...signers: [{id: 'signerOne',name: 'Stevie Signer',signerType: 'embedded',// `enableEmails: true | undefined` (default) enables all emails// `enableEmails: false | []` disables all emails// `enableEmails: ['...', '...']` enables specified emailsenableEmails: ['etchComplete']...}]}
Supported emails
'etchComplete'
- signer will receive an email after everyone has completed signing with a link to download the documents.
redirectURL
If provided, the signer will be redirected to the URL (by the browser; localhost allowed for development) once they've completed their signatures. If not provided, the signer will be redirected to an Anvil page indicating state of the packet, e.g. "Waiting on other people to sign", "Everyone has signed", "Error!" etc.
Here's an example of an embedded signer scenario:
{...signers: [{id: 'signerOne',name: 'Stevie Signer',signerType: 'embedded',redirectURL: 'https://yoursite.com/signer-complete',...}]}
When the signer is finished signing, redirectURL
will receive several query params to help you display a relevant page to the user or update state in your system. e.g.
GET https://yoursite.com/signer-complete?action=signerComplete&signerStatus=completed&signerEid=SI8xMh51WBR9dyGILDoL&nextSignerEid=s1AFrmQeuj3qMchKmhL2&documentGroupStatus=completed // completed || partial || sent&documentGroupEid=GqqU9OKLhmnGBeCusRRa&etchPacketEid=rmmhL2s1AQeuj3qhKFMc
If there is an error while signing or navigating to the signing page, redirectURL
will receive error params in addition to the query params above. For example, if the user clicks a link with an expired token, your URL will be called with error-specific parameters:
GET https://yoursite.com/signer-complete?action=signerError&errorType=tokenExpired // tokenExpired || tokenInvalid || notFound || unknown&error=Token+Expired&message=Error+specific+message&signerStatus=sent&signerEid=SI8xMh51WBR9dyGILDoL&documentGroupStatus=sent&documentGroupEid=GqqU9OKLhmnGBeCusRRa&etchPacketEid=rmmhL2s1AQeuj3qhKFMc
For errors including an invalid token or non-existent signer, query parameters will only contain error information and the signerEid
.
GET https://yoursite.com/signer-complete?action=signerError&errorType=tokenInvalid&error=Invalid+Token&message=Error+specific+message&signerEid=SI8xMh51WBR9dyGILDoL
fields
They must also contain a fields
key which is an array of SignerField objects specifying which fileId
+ fieldId
combinations each signer is responsible for. Each fileId
comes from the id
you specified in each of the files
elements within the call to this mutation. Each fieldId
comes from either a field defined in the files
key, or the PDF Template UI.
See creating a PDF template for more information on getting field IDs for templates.
See using text tags as an alternative to manually mapping fields. Text tags can automatically map signature fields to signers via text embedded in the doc.
{files: [{id: 'uploadedFile',file: {stream object for file to upload},fields: [{id: 'signatureOne',type: 'signature',...}]}, {id: 'templatePDF',castEid: '5GshL2s1AQeuj3qhKFMc'// In a template PDF, the fields are setup in the UI}],signers: [{id: 'signerOne',name: 'Stevie Signer',email: 'steve.signer@example.com',routingOrder: 1,fields: [{fileId: 'uploadedFile', // The file `id` from abovefieldId: 'signatureOne', // The field `id` from above...}, {fileId: 'templatePDF', // The file `id` from abovefieldId: 'signatureOne', // The field `id` in the PDF template available in the UI}]}],...}
tokenValidForMinutes
When a signer visits a URL to sign, the URL will contain an expiring SignerToken
. You can control the duration of this token on a per-signer basis with tokenValidForMinutes
. When the signing URL is generated, it will add tokenValidForMinutes
to the current time to determine the token expiration date.
Regenerating the signing URL will restart the token duration. e.g. If you set tokenValidForMinutes
to 60 * 24 * 3
(3 days), then after 1 day, you resend the email asking a user to sign, the new email will have a link with a 3 day token duration.
Default token durations are different depending how the signer is setup:
signerType: 'email'
- Default duration is 7 days.signerType: 'embedded'
- Default duration is 2 hours. It is recommended you keep this short. URLs for embedded signers are intended to be generated at the time the user is ready to sign. i.e. You should not be emailing an Anvil signing link to your users. See the recommended flow section of this document for more information.
You can control what happens when the user experiences a token expiration.
{...signers: [{id: 'signerOne',name: 'Sally Signer',signerType: 'email',tokenValidForMinutes: 60 * 24 * 3 // 3 days...}]}
Using text tags
All documents support text tags, including uploaded documents, PDF templates, and generated documents. Text tags are pieces of text embedded in a document that you can use like variables to specify where to place signatures, names, etc. e.g. You would write {{signature}}
or {{initial:::signer1}}
into your document, and Anvil will pick them up as fields.
Text tags allow you to automatically assign signature fields to signers when creating a signature packet.
The previous signers section outlined a fields
property to map fields to signers. With text tags that specify the signerID
argument, you do not need to map fields to signers in the fields
property.
As an example, consider this NDA. Note the signer1
and signer2
at the end of the each tag. Those strings will help anvil auto-assign the fields to signers with the same ids.

We'll make the text tags have a white font color, so Anvil can find them, but your users do not see them.

Then we can use the file in our script
const employeeEmail = 'employee@example.com'const employerEmail = 'employer@example.com'const employeeName = 'Sally Employee'const employerName = 'Jane AcmeManager'// You can upload a PDF or docx file with text tagsconst tagsPDFFile = Anvil.prepareGraphQLFile('path/to/text-tags-nda.pdf')const createEtchPacketVariables = {files: [{// This is a file we will upload and specify the fields ourselvesid: 'fileUploadTextTags',title: 'Demo Text Tags NDA',file: tagsPDFFile,fields: [], // fields will be populated by text tags},{// You can upload a template with text tags that contain signer ids.// The signer ids will be retained in the template and automatically// assigned to the new packet's signers that have a matching id.id: 'templateTextTags',title: 'Text Tags Template',castEid: 'nA1clF5gMu7wjtDHcpCM',},{// Text tags work in generated files as wellid: 'generatedTextTags',filename: 'gen-invoice.pdf',title: 'Generated text tags',fontSize: 12,textColor: '#222222',fields: [{label: 'Hello',content: 'hi',},{label: 'First Signer',},{textColor: '#FFFFFF',content:'{{ signature:::signer1 }} {{ initial:::signer1 }} {{ signatureDate:::signer1 }}',},{label: 'Second Signer',},{textColor: '#FFFFFF',content:'{{ signature:::signer2 }} {{ initial:::signer2 }} {{ signatureDate:::signer2 }}',},],},],data: {// You can fill text tags with data by using their field alias// e.g. your doc has `{{shortText :: myField}}`// then you specifypayloads: {fileUploadTextTags: {// file id with the text tagdata: {myField: 'My Text', // fieldAlias: 'fill data'},},},},signers: [// Signer signature fields will be assigned by textTags{id: 'signer1',name: employeeName,email: employeeEmail,// `fields` will be populated by textTags!},{id: 'signer2',name: employerName,email: employerEmail,// `fields` will be populated by textTags!},],}
See the text tags help article for full details on text tags formatting.
Filling your PDFs with data
You may need to fill out non-signature fields in the PDFs in your Packet. We support this via a payloads
object nested in the data
key. For each PDF you'd like to fill, provide a fill payload object in the data.payloads
object under a key that is the id
for the PDF from the Template files configuration:
{...files: [{id: 'somePdf',castEid: 'ABC123'}],data: {payloads: {// This key comes from the `id` in the `files` array abovesomePdf: {// Your fill payload here...}}}}
See creating a PDF template for more information on creating templates and fetching their field IDs.
Encrypting data payloads
If you are working with sensitive data, you can encrypt the data that fills your PDFs. Setup an RSA keypair, then encrypt the data
key's JSON string with your public key. You can use our node encryption library.
{...data: 'an encrypted string'files: [{id: 'somePdf',castEid: 'ABC123'}],}
Sending the Packet out for signatures
If you would like to immediately create a DocumentGroup
so that you can begin gathering signatures, you can do so by adding isDraft: false
to your mutation variables. The default is true
, and means that we will not create document group and kick things off. If you want our system to begin sending emails to signers, or you'd like to request embedded signer URLs, you will need to set isDraft: false
.
Testing your packet configuration
If you would like to test that your API calls are properly configured and that your signer fields will all be filled out properly, you can do so by adding isTest: true
to your mutation variables. When your Packet is a test, everything in our system will behave normally except that:
- Documents will be watermarked with a demo indicator.
- Signatures will be in red.
- This Packet will not count against your plan's e-signature quota.
You can view the results of Test Packet completions by filtering for Test
in the Etch e-sign
area:

Customizing the signature page
Strings and colors on the signature page are configurable via the signaturePageOptions
variable on createEtchPacket
.
A page with only custom strings specified:

A page with custom colors specified:

All the options and their defaults:
// Template replacements are supported by all strings.// Available template replacements:// {// organization: { name },// packet: { name },// signer: { name },// currentUser: { name },// }signaturePageOptions: {title: 'Add Your Signature',// Description supports markdowndescription: '__{{organization.name}}__ requests your signature __({{signer.name}})__ on the following documents for __{{packet.name}}__.',signatureLabel: 'signature',initialLabel: 'initials',acceptTitle: 'Accept Your Signature',acceptDescription: 'Below is how your signature and initials will appear everywhere you need to sign on all documents.',acceptButtonText: 'I Accept My Signature And Initials',drawSignatureTitle: 'Draw Your Signature',drawSignatureDescription: 'Your signature will appear on all documents where you need to sign.',drawSignatureButtonText: 'I Accept My Signature',drawInitialsTitle: 'Draw Your Initials',drawInitialsDescription: 'Your initials will appear on all documents where you need to initial.',drawInitialsButtonText: 'I Accept My Initials',signTitle: 'Sign All Documents',signDescription: 'Click below to sign and date on all documents.',signDescriptionCompleted: 'Documents have been completed and signed.',signConsentText: 'I have reviewed the documents and I consent to using electronic signatures.',signButtonText: `Sign {{packet.name}} Documents`,completedButtonText: 'Go To Download Page',error: 'Oops there was an error:',// Page color customization. We will programmatically generate related colors// like text and hover colors based on the colors you choose.style: {primaryColor: '#1985a1', // Buttons, title underline, loading spinnersuccessColor: '#1985a1', // Completed actionsinfoColor: '#46494c', // Info actions, uncompleted itemslinkColor: '#1985a1', // Links// See the section below for more white labeling options},}
White labeling with a custom stylesheet
You can specify a custom stylesheet injected into the signature page for all signers. This stylesheet can be served from your website giving you full control over the custom styles.
White labeling with custom stylesheets is an enterprise feature. Please contact sales and let them know your use case.
signaturePageOptions: {style: {// Specify a custom stylesheet served from your website that will be// injected into the signature page for all signers.stylesheetURL: 'https://example.com/anvil-styles.css'},}
Customizing your logo
You can specify a custom logo that will be shown
- At the top of the signature page
- In emails sent to the signers
Your logo will be shown at a maximum of 30px
high and 100% wide.
All you need to do is upload a logo on your organization settings. Make sure you save the settings after uploading your logo:

When you send a packet, it will automatically use your logo at the top

Customizing signer emails
You can customize email subject, body content and the reply-to email header with these options:
signatureEmailSubject
Customizes the email subject shown to signers. By default, this will be the name of your signature packet.
signatureEmailBody
Customizes the email body shown to signers. By default, the email will contain instructions on how to sign. When this is used, signatureEmailBody
will be placed alongside signing instructions. It will not replace the signing link and instructions.
replyToName
Customizes the reply-to name shown when a user attempts to reply to the signing email. By default this will be your organization's name.
replyToEmail
Customizes the reply-to email shown when a user attempts to reply to the signing email. By default this will be the support email listed in your organization settings.
Tying it all together
Here's an example mutation that touches on the things discussed here:
createEtchPacket({variables: {isDraft: false,isTest: true,name: 'A New Etch Packet',signers: [{id: 'clientSigner',routingOrder: 1,signerType: 'embedded',redirectURL: 'https://mysite.com/signup-complete',name: 'Sally Client',email: 'sally@example.com',fields: [{ fileId: 'existingPdf', fieldId: 'signatureOne' },{ fileId: 'newPdf', fieldId: 'signatureThree' }]},{id: 'complianceSigner',routingOrder: 2,name: 'Larry Lawyer',email: 'legal@example.com'fields: [{ fileId: 'existingPdf', fieldId: 'signatureTwo' },{ fileId: 'newPdf', fieldId: 'signatureFour' }]}],files: [{id: 'existingPdf',castEid: existingPdf.eid,},{id: 'newPdf',file: <Upload>, // See Upload reference for more detailsfields: [{id: 'someNonSignatureField',type: 'text',pageNum: 1,rect: { x: 253.12, y: 121.12, width: 33.22, height: 27.22 }},{id: 'signatureThree',type: 'signature',pageNum: 1,rect: { x: 203.11, y: 171.11, width: 33.11, height: 27.11 }},{id: 'signatureFour',type: 'signature',pageNum: 2,rect: { x: 253.12, y: 121.12, width: 33.22, height: 27.22 }}]},],data: {payloads: {newPdf: {data: {someNonSignatureField: 'Some non-signature Value'}},existingPdf: {data: {anotherNonSignatureField: 'Another non-signature Value'}}}},signaturePageOptions: {// String overrides for the UI},// Customize your emailsignatureEmailSubject: 'Please fill out this form',signatureEmailBody: 'You will need your drivers license to fill out this form. Please have that available.',// With these options, for example, the reply-to email header will look like "Jane Doe <jdoe@example.com>"replyToName: 'Jane Doe',replyToEmail: 'jdoe@example.com'}}) : EtchPacket
The mutation will return an EtchPacket. You can see more details on the full mutation schema here.
Want to see the code in action? Create your first signature packet by running our ready-to-go example on Postman.
Sending a signature packet
Sending the packet must be done before you can gather signatures, even if your signers are embedded
signers. Before sending an EtchPacket
, the packet will be in draft
mode.
If you want to send the packet immediately on creation, use isDraft: false
in the createEtchPacket
mutation call.
If your packet is created, but has not been sent, use the sendEtchPacket
mutation.
- For packets in draft mode (for example
isDraft
set to true in thecreateEtchPacket
mutation),sendEtchPacket
will kick off the signature collection process by sending an email to the first signer. - Calling after the signature process has started will 'resend' an email to the current signer in the routing order.
Simply call the following mutation with the eid
of the EtchPacket you have already created:
sendEtchPacket({eid: yourExistingPacket.eid})
Note that this only has an effect if the first/next signer is of signerType: "email"
.
See our example on Postman here.
Controlling the Signature Process with Embedded Signers
By default, we will solicit all signatures via email. However, you can embed the signature process into your app or website.
By setting the signerType
of any signer in your Packet to "embedded"
, that signer will not be sent an email when it's time to sign. It will be up to you to get the signer to complete their signatures via a sign URL generated by the generateEtchSignURL
mutation.
generateEtchSignURL (# The eid from the Signer in questionsignerEid: String!,# The signer's user id in your system## NOTE: It's important that you set `clientUserId`# to the user ID in your system. It provides# traceability. E-signatures are only valid if the# person signing can be verified with some level# of confidence that they are who they say they are.# The `clientUserId` provides that traceability as# presumably your app has verified the signer's# email address.clientUserId: String!): String # The URL
The signing URL from generateEtchSignURL
will include a token with a 2 hour expiration timestamp from when it was generated.
Redirect the user to the resulting URL, or embed the URL in an iframe. If you have set up the signer's redirectURL
, they will be redirected back to your app when they are finished signing their documents.
Recommended flow
The default token expiration on the signing URL is short by design. It is intended to be generated at the moment the user is ready to sign their documents. When it is time for your user to sign documents, it is recommended the user visits a URL in your system. That route should verify the user is who they say they are, call generateEtchSignURL
, then immediately redirect them to (or embed) the fresh signing URL.
For example, say you want a user to sign from an email you have sent from your system:
- You send Sally Jones an email with a
Sign now
button. - The
Sign now
button should point to a URL in your system, e.g.https://yourco.com/sign/sally-jones
. - Your
/sign/sally-jones
route handler verifies Sally is actually Sally (e.g. she can log in). - Your system calls
generateEtchSignURL
to generate the signing URL. - Your system redirects Sally to (or embeds) the URL from
generateEtchSignURL
. - If sally does not sign during that session, you can call
generateEtchSignURL
again to regenerate the signing URL when she is ready to sign. Old tokens, even if they are not beyond the expiry date, will be expired on subsequentgenerateEtchSignURL
calls; only the newest token will be available for signing.
Signer information
The signerEid
s for your Packet can be found in the response of your createEtchPacket
mutation call via a response query like the following:
createEtchPacket(...,signers: [{id: 'signerOne',routingOrder: 1,...}]) {# Response query. Top level item is an EtchPacketeidnamedocumentGroup {eidsigners {eidaliasId # From the 'id' you gave each signer. E.g. 'signerOne'routingOrder # From the `routingOrder` you specified, or location in the signers array}}}
You can use either the aliasId
or the routingOrder
to correlate a response signer with the signers you provided in your mutation variables, and from there you can pluck out the eid
to be used as the signerEid
variable in your generateEtchSignURL
mutation call.
Already created a signature packet? Try running our example on Postman to generate your signature link.
Embedding the signing UI in an iframe
Make sure you read about controlling the signing process with embedded signers above. In order to embed the signing process in your app, you will need to use embedded signers. That means:
- Set the signer's
signerType
to"embedded"
when creating your packet. Each signer who will be shown the signing UI in an iframe needs to besignerType: 'embedded'
. - Use the
generateEtchSignURL
mutation to generate a signing URL for your signer. Only signing URLs generated bygenerateEtchSignURL
can be embedded in aniframe
.
React components
If you use React, the easiest way to embed the signing UI is to use our AnvilSignatureFrame
or AnvilSignatureModal
React components found in our React-UI library. These components will handle lifecycle callbacks for you.
Vanilla iframe
When it is time for your user to sign, generate a URL from via the generateEtchSignURL
mutation, and use the URL from the response in the src
attribute of the iframe
. For now you will need to append a withinIframe=true
query parameter to the end of the response URL.
const newURL = generateEtchSignURL({ variables: { signerEid, clientUserId } })const urlToFrame = newURL + '&withinIframe=true'
Then use it in the src
attribute of your iframe
.
<iframe src={urlToFrame}></iframe>
After signing or when the user experiences an error, Anvil will communicate with the parent window using the Window.postMessage()
API. To receive the message, you will need to add an event listener to your app. We recommend the following:
window.addEventListener('message', ({ origin, data }) => {if (origin !== 'https://app.useanvil.com') returnif (data && typeof data === 'object') {// You will receive the event object when the signing process is complete, or// the user sees an error. This object will contain several properties:// `action`, `signerStatus`, `signerEid`, `nextSignerEid`, `documentGroupStatus`,// `documentGroupEid`, `etchPacketEid`, `weldDataEid`.//// e.g.//// {// action: 'signerComplete',// signerStatus: 'completed',// signerEid: 'Jc1ZJisPnpF5lHyfyqBW',// nextSignerEid: 'WBqyfyHl5FpnPsiJZ1cJ',// documentGroupStatus: 'partial',// documentGroupEid: 'nEKq2eGim0ijSqKd98nG',// etchPacketEid: 'XzfmVPfGUEyBc1XyylFo',// }//// When there is an error, you will also receive `errorType`, `error`, and// `message` properties.//// You do not need to trigger a redirect, the iframe will make the redirect.if (data.action === 'signerComplete') {// A signer has finished signing} else if (data.action === 'signerError') {// A signer has experienced an error}}})
The properties on the object passed to the postMessage
handler will be the same as the query params passed to the signer's redirectURL
attribute. For full details on the redirectURL
query params, see the redirectURL
section above.
If you have a redirectURL
setup on your signer, the iframe
will be redirected to your redirectURL
when finished signing or on error. It is not required to wait for the redirect to finish loading. It is fine to remove the iframe
from the page when you have received the postMessage
event.
Enabling iframe embedding
Out of the box, the following can be embedded for testing purposes in any URL (including localhost
):
EtchPacket
s created with your dev API keyEtchPacket
s created with your production key andisTest: false
You can enable embedding of production packets from the API tab in your organization's settings.

Once you enable embedding, you will be asked to add your domains to a whitelist:

Troubleshooting embedded signers
Sometimes things go wrong! That's ok, here are some common problems and steps to resolve them. If after reading this, you are still having issues, please contact support, and we can help sort it out.
The iframe
is not allowing the page to be framed
- Are you framing a URL returned from
generateEtchSignURL
? Only signing URLs returned fromgenerateEtchSignURL
can be framed. - Is your packet created with
isTest: false
?- If so and the packet is for testing purposes, use
isTest: true
. - If so and the packet is for production purposes, enable iframe embedding in your org settings.
- If so and the packet is for testing purposes, use
- Are you using your production API key?
- Make sure iframe embedding is enabled
- Did you append
&withinIframe=true
to the end of your URL?- Without this parameter, even
isTest: true
packets will be blocked from framing in certain scenarios.
- Without this parameter, even
- Is the failing parent URL included in your trusted domains? It's possible the URL rendering the
iframe
is not in the list of allowed URLs. The scheme, subdomains (or*
), domain, and port need to match. e.g.https://example.com
andhttps://sign.example.com
are different.
The generateEtchSignURL
mutation returns null
- Is your user setup with
signerType: 'embedded'
? Email signers will be sent an email when it is their turn to sign.- Set
signerType
to embedded.
- Set
My packet does not have a DocumentGroup or Signers
- Is your packet in
draft
mode? - The
generateEtchSignURL
mutation requires a signerEid, and the simplest way to fetch that info is in the response of yourcreateEtchPacket
call. If you have a packet, but fetching the signer information returnsnull
, your packet is indraft
mode. The packet will need to be "sent", either by specifyingisDraft: false
on creation, or by callingsendEtchPacket
.
Webhook notifications
Anvil can send POST requests to your servers when certain events happen. To receive these messages, please enable webhooks. Check out the e-signature webhook section for all supported actions.
Packet-specific webhook URLs
Each packet can have its own webhook URL via the webhookURL
variable on createEtchPacket
.
{...webhookURL: 'https://example.com/custom-webhook',files: [...],}
Your webhookURL
will receive a POST request with data when some actions take place. See the per-object webhook URL docs for more info.
{action: 'signerComplete',token: '38Gp2vP47zdj2WbP1sWdkO2pA7ySmjBk', // from the webhook setupdata: 'see the docs',}
Downloading documents
When all parties have signed, you can fetch the completed documents in zip form from the following URL:
GET https://app.useanvil.com/api/document-group/${documentGroupEid}.zip
Use your API key to authenticate to this URL.
Best practice is to save the completed documents to your own object store when the last signer has finished. Fetch and save either on the etchPacketComplete webhook notification, or in your route handling a signer's redirectURL.
A couple helpers exist to help with downloading
- The Anvil node client provides a
downloadDocuments(documentGroupEid)
function to download as a buffer or stream. - DocumentGroup has a
downloadZipURL
resolver returning the download URL.
Have all signers signed? Try downloading your completed documents by using the code in our Postman example.
Handling signing errors
There are a couple errors that your users may encounter while attempting to sign documents. For example your user hits a signing URL with an expired or invalid token!
By default we will show an error page, but you can handle these errors in your system to provide a better experience. There are two ways we can notify you when this happens:
- Redirect to the URL you have set as
redirectURL
on the signer - Post a message to the parent frame when the signing URL is embedded in an iframe
Along with each of these notifications, you will receive several pieces of information you can use to determine the signer and error info. See the sections about redirectURL
and embedding for details on how this info will be formatted, and other data you may receive alongside these error attributes.
action
:'signerError'
Signing errors will always have this actionerrorType
: One of the followingtokenExpired
: The token specified has expired, or once was valid but no longer is.tokenInvalid
: The token is not associated with this signer. Beware! Do not recover from this error. It indicates that someone attempted to use an incorrect, potentially nefarious tokennotFound
: The signer EID was not found
error
: The title of the errormessage
: The message of the errorsignerEid
: The AnvilSigner
EID that was attempting to sign
Recovering from token errors
When a user encounters a token error, you can recover by issuing the user a new signing URL. Before you generate a new signing URL, please ensure a few things:
errorType
is'tokenExpired'
. The onlyerrorType
you should recover from istokenExpired
!- The user who is attempting to sign is the user who should be signing (for example, you ensure they are logged in)
- The signer has not already finished signing (
signer.status != 'completed'
)
The method you use to generate the new signing URL depends on the Signer's signerType
.
Email signers
If the signer is set up to sign by way of an email from Anvil, they will need a new email with a new signing URL from Anvil. Call the sendEtchPacket
GraphQL mutation to send the signer a new email. You will specify etchPacketEid
to which the signer is attached, not signerEid
, and it will send an email to the first un-signed signer:
mutation sendEtchPacket($eid: String!) {sendEtchPacket(eid: $eid) {eidnamestatus}}
Then call it with etchPacketEid
etchPacket = sendEtchPacket({ variables: { eid: etchPacketEid } })
Embedded signers
For embedded signers, you can use generateEtchSignURL
to generate a fresh URL and token for the user.
mutation generateEtchSignURL($signerEid: String!, $clientUserId: String!) {generateEtchSignURL(signerEid: $signerEid, clientUserId: $clientUserId)}
Then call it like you would for a new signer:
newURL = generateEtchSignURL({ variables: { signerEid, clientUserId } })
And redirect the user to the new URL.
Fetching token metadata
You may want to know token expiry dates to preemptively handle upcoming token expirations. You can fetch validity and expiration information via the signerTokens
resolver on the Signer
GraphQL object. The most effective way to fetch this information is through an etchPacket
:
query etchPacket($eid: String!) {etchPacket(eid: $eid) {eidnamestatusdocumentGroup {eidstatussigners {eidstatussignerTokens {eidtypevalidUntilvalidinvalidatedAthasSignedsignedAt}}}}}