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

example

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 a node API client that wraps authentication and makes 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:

  1. references to existing PDF Templates that you have already set up.
  2. completely new documents that you can upload + configure in the mutation call.
  3. 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 API
castEid: '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.

We support 2 flavors of new document uploading which will be provided in the file property of each upload:

  1. 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.
  2. A Base64 Upload that allows you to send a file that you have in a base64-encoded string format.

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 page
pageNum: 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 new PDF

Anvil can generate PDFs to include in your signature packet if you don't have an existing template or PDF. In this case, the filename property needs to be supplied.

When creating a new PDF for your signature packet, you must provide a title string 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.

Here is an example of how this is structured:

{
...
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. However, if you'd like to embed the signature process into one of your own flows we support this as well. By setting the signerType to "embedded", 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.

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 emails
enableEmails: ['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) 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", 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
?signerStatus=completed
&signerEid=SI8xMh51WBR9dyGILDoL
&nextSignerEid=s1AFrmQeuj3qMchKmhL2
&documentGroupStatus=completed // completed || partial || sent
&documentGroupEid=GqqU9OKLhmnGBeCusRRa
&etchPacketEid=rmmhL2s1AQeuj3qhKFMc

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.

{
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 above
fieldId: 'signatureOne', // The field `id` from above...
}, {
fileId: 'templatePDF', // The file `id` from above
fieldId: 'signatureOne', // The field `id` in the PDF template available in the UI
}]
}],
...
}

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 above
somePdf: {
// 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 document group 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:

  1. Documents will be watermarked with a demo indicator.
  2. Signatures will be in red.
  3. 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:

Test Packet

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:

custom-e-signature-page

A page with custom colors specified:

custom-e-signature-page

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 markdown
description: '__{{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 spinner
successColor: '#1985a1', // Completed actions
infoColor: '#46494c', // Info actions, uncompleted items
linkColor: '#1985a1', // Links
},
}

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:

custom e-sign logo

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

custom logo on signature page

Tying it all together

Here's an example mutation that touches on the things discussed here:

createEtchPacket({
variables: {
send: true,
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 details
fields: [
{
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
}
}
}) : 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

Use the sendEtchPacket mutation to send a signature email to the current

  • For packets in draft mode (for example isDraft set to true in the createEtchPacket 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, if you'd like to embed the signature process into one of your own flows we support this as well.

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 question
signerEid: 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

Redirect the user to the resulting URL. If you have setup the signer's redirectURL, they will be redirected back to your app when they are finished signing their documents.

If you would like to embed the signing process in an iframe, check out this section.

The signerEids 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 EtchPacket
eid
name
documentGroup {
eid
signers {
eid
aliasId # 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 process within an iframe

Please contact us if you would like this feature to be enabled.

After signing, 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: redirectURL }) => {
if (origin !== 'https://app.useanvil.com') return
// You will receive the redirectURL once the signing process is complete
})

To skip this setup, you can use the AnvilSignatureFrame or AnvilSignatureModal React components found in our React-UI library which handles this process for you.

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 setup
data: '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.

Get started today

Start filling, generating, and signing PDFs from your app. Every account comes with free access to the Developer API.