Box Sign: Custom notifications and new webhooks

Rui Barbosa
Box Developer Blog
Published in
7 min readJun 11, 2024

--

Image by vectorjuice on Freepik

Our new Box Sign related webhooks — now combined with the ability to suppress notifications — give you complete control over how your application informs users about a signing process.

By default, Box Sign sends notifications to users by email under the box.com domain. Although you can customize these emails, many customers prefer to use their own email system, or go beyond email with their own notification systems.

In this article, we’ll explore the implementation details of suppressing default notifications using the Python Next Gen SDK.

⚠️ When you choose to suppress Box email notifications, your organization assumes responsibility for ensuring the delivery of all notifications to signers at the appropriate time in the signing process (and with the appropriate content). Organizations must also comply with applicable laws and regulations, including requirements to obtain signer consent for the delivery methods used, if applicable.

It’s all about webhooks

In our example below, when we suppress the Box Sign notifications, our applications will need to figure out if there is a state change on a signature request. We could do this by polling the request status, but that’s not very practical. Ideally we should use webhooks, so that our app gets notified of the changes.

For this, Box has created 3 new webhooks related to Box Sign:

  • SIGN_REQUEST.SIGNATURE_REQUESTED — When a signature request is created for a individual signer
  • SIGN_REQUEST.SIGNER_SIGNED — When an individual signer has completed a signature
  • SIGN_REQUEST.ERROR_FINALIZING — When there is an error in the last steps of the signature request (for example, the system can’t generate the signed document)

In this exercise, we’ll capture most Box Sign-related webhooks using webhook.site, an awesome tool that enables us to see the webhook payload.

After obtaining a unique URL from webhook.site, we create the webhook under the relevant application:

All Sign-related webhooks monitoring a folder

Note: You should always generate the webhook keys associated with your application, so that your application can verify the authenticity of the webhook request.

Signature requests

To simplify the signature request options and focus on the notification suppression and webhooks, I’ve created a template to use in this exercise:

  • Simple-PDF — A single-signer sample document

This makes the request fairly simple.

Using default notifications

This request:

curl --location 'https://api.box.com/2.0/sign_requests' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer DT...0v' \
--data-raw '{
"parent_folder": {
"id": "249194622181",
"type": "folder"
},
"template_id":"15422517-8adb-42e1-895d-bbe1cf02aafd",
"signers": [
{
"email": "barbasr+signerA@gmail.com",
"role": "signer"
}
]
}'

Returns a signature request object (simplified):

{
"is_document_preparation_needed": false,
"signers": [
{
"email": "barduinor@gmail.com",
"role": "final_copy_reader",
"suppress_notifications": false
},
{
"email": "barbasr+signera@gmail.com",
"role": "signer",
"suppress_notifications": false
}
],
"id": "ae4b3058-78b8-4526-bb21-d6582ddd383f",
"parent_folder": {
"id": "249194622181",
"name": "signed docs"
},
"name": "Simple-PDF.pdf",
"type": "sign-request",
"status": "created",
"sign_files": {
"files": [
{
"id": "1538251021756",
"name": "Simple-PDF.pdf",
}
],
"is_ready_for_download": true
},
"template_id": "15422517-8adb-42e1-895d-bbe1cf02aafd",

}

And the webbhook sent this payload (simplified):

{
"type": "webhook_event",
"id": "4548de33-a405-47af-981b-75f722c2f7ee",
"created_at": "2024-05-22T08:10:57-07:00",
"trigger": "SIGN_REQUEST.SIGNATURE_REQUESTED",
"webhook": {
"id": "2770072773",
"type": "webhook"
},
"source": {
"id": "1538251021756",
"type": "file",
"name": "Simple-PDF.pdf",
"item_status": "active"
},
"additional_info": {
"sign_request_id": "ae4b3058-78b8-4526-bb21-d6582ddd383f",
"signer_emails": [
"barduinor@gmail.com",
"barbasr+signera@gmail.com"
],
"external_id": ""
}
}

Notice the payload is telling your app that a signature request was created. It includes the document associated with it, the signers’ emails, and (most importantly) the signature request ID.

As I complete this request happy path, we get a few more requests back from the webhook.

When the signer signs the document (notice the specific signer details):

{
"type": "webhook_event",
"id": "b6bf1a5b-7346-4cda-b959-4c89d078db6f",
"created_at": "2024-05-22T08:28:56-07:00",
"trigger": "SIGN_REQUEST.SIGNER_SIGNED",
"webhook": {
"id": "2770072773",
"type": "webhook"
},
"additional_info": {
"sign_request_id": "ae4b3058-78b8-4526-bb21-d6582ddd383f",
"signer_emails": [
"barduinor@gmail.com",
"barbasr+signera@gmail.com"
],
"single_signer_event_email": "barbasr+signera@gmail.com",
"single_signer_details": {
"embed_url_external_user_id": null,
"role": "signer",
"order": 1
},
}
}

When the signature request is completed:

{
"type": "webhook_event",
"id": "20dda22c-5862-4d4f-876c-94a266705e14",
"created_at": "2024-05-22T08:28:56-07:00",
"trigger": "SIGN_REQUEST.COMPLETED",
"webhook": {
"id": "2770072773",
"type": "webhook"
},
"created_by": {
"type": "user",
"id": "18622116055",
"name": "Rui Barbosa",
"login": "barduinor@gmail.com"
},
"additional_info": {
"sign_request_id": "ae4b3058-78b8-4526-bb21-d6582ddd383f",
"signer_emails": [
"barduinor@gmail.com",
"barbasr+signera@gmail.com"
],
"external_id": ""
}
}

I’m showing simplified versions — the webhook payload is much more dense. However, if you are missing some information, you can always query the GET /2.0/signs_requests/:sign_request_id and inspect the full details.

Suppressing notifications

To suppress notifications, you must include the suppress_notifications flag for each signer and the embed_url_external_user_id. Without both of these flags, the API will return a 404 bad request (since you wouldn’t get the signature URL you need to request that the user sign the document).

For example:

curl --location 'https://api.box.com/2.0/sign_requests' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer DT...0v' \
--data-raw '{
"parent_folder": {
"id": "249194622181",
"type": "folder"
},
"template_id":"15422517-8adb-42e1-895d-bbe1cf02aafd",
"signers": [
{
"email": "barbasr+signerA@gmail.com",
"role": "signer",
"suppress_notifications":true
}
]
}'

Returns:

{
"type": "error",
"code": "bad_request",
"status": 400,
"message": "Bad request",
"help_url": "https://developer.box.com/guides/api-calls/permissions-and-errors/common-errors/",
"request_id": "1fe9d1228d02f19b8efd4a3ca5b664ed9",
"context_info": {
"errors": [
{
"name": "Bad Request",
"message": "Invalid request payload input",
"reason": "invalid_parameter"
}
]
}
}

A proper request would be:

curl --location 'https://api.box.com/2.0/sign_requests' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer DT...0v' \
--data-raw '{
"parent_folder": {
"id": "249194622181",
"type": "folder"
},
"template_id":"15422517-8adb-42e1-895d-bbe1cf02aafd",
"signers": [
{
"email": "barbasr+signerA@gmail.com",
"role": "signer",
"suppress_notifications":true,
"embed_url_external_user_id":"my_id"
}
]
}'

Returns:

{
"is_document_preparation_needed": false,
"signers": [
{
"email": "barduinor@gmail.com",
"role": "final_copy_reader",
"embed_url": null,
"iframeable_embed_url": null,
"suppress_notifications": false
},
{
"email": "barbasr+signera@gmail.com",
"role": "signer",
"embed_url_external_user_id": "my_id",
"embed_url": "https://app.box.com/sign/document/e8715868-e88d-4f98-8da6-6de91c73ff08/1060eef0a2230e928ad43c868d08d6807d5bbe42e642579b277975b1cc8725c6/",
"iframeable_embed_url": "https://app.box.com/embed/sign/document/e8715868-e88d-4f98-8da6-6de91c73ff08/1060eef0a2230e928ad43c868d08d6807d5bbe42e642579b277975b1cc8725c6/",
"suppress_notifications": true
}
],
"id": "e8715868-e88d-4f98-8da6-6de91c73ff08",
"name": "Simple-PDF (1).pdf",
"type": "sign-request",
"status": "created",
"template_id": "15422517-8adb-42e1-895d-bbe1cf02aafd",
"external_system_name": null
}

As you can see above, we’ve generated an embed_url where the signer can actually sign the document. It’s now up to your app to facilitate this for your user.

We’ve also generated another callback from the webhook:

{
"type": "webhook_event",
"id": "57457ae5-7aae-4624-b4bb-75c1a3b4d6e2",
"created_at": "2024-05-22T08:53:42-07:00",
"trigger": "SIGN_REQUEST.SIGNATURE_REQUESTED",
"webhook": {
"id": "2770072773",
"type": "webhook"
},
"additional_info": {
"sign_request_id": "e8715868-e88d-4f98-8da6-6de91c73ff08",
"signer_emails": [
"barduinor@gmail.com",
"barbasr+signera@gmail.com"
],
"external_id": ""
}
}

This payload isn’t much different than the one before we suppressed notifications. Note that you don’t have all the information you need to send notifications (e.g. who the signer is), and this forces your application to get more details from the signature request.

One way to complete the signature request is to open the embed_url:

Completing the signature request

After the user completes the signature request, we get a couple more webhook callbacks.

When the specific user signs:

{
"type": "webhook_event",
"id": "9ff2eebd-da90-4609-8ffd-e6c27eb46c01",
"created_at": "2024-05-22T09:07:25-07:00",
"trigger": "SIGN_REQUEST.SIGNER_SIGNED",
"webhook": {
"id": "2770072773",
"type": "webhook"
},
"additional_info": {
"sign_request_id": "e8715868-e88d-4f98-8da6-6de91c73ff08",
"signer_emails": [
"barduinor@gmail.com",
"barbasr+signera@gmail.com"
],
"external_id": "",
"single_signer_event_email": "barbasr+signera@gmail.com",
"single_signer_details": {
"embed_url_external_user_id": "my_id",
"role": "signer",
"order": 1
}
}
}

When the signature request process is complete:

{
"type": "webhook_event",
"id": "2cb045f0-553e-4cb3-931d-aef208b4e3a0",
"created_at": "2024-05-22T09:07:25-07:00",
"trigger": "SIGN_REQUEST.COMPLETED",
"webhook": {
"id": "2770072773",
"type": "webhook"
},
"additional_info": {
"sign_request_id": "e8715868-e88d-4f98-8da6-6de91c73ff08",
"signer_emails": [
"barduinor@gmail.com",
"barbasr+signera@gmail.com"
],
"external_id": ""
}
}

The not-so-happy path

What happens if the signer refuses to sign the document?

Using a similar request as above, the signature request is created, and the webhook fires the call back, returning a SIGN_REQUEST.SIGNATURE_REQUESTED.

When the signer declines the request we get a SIGN_REQUEST.DECLINED. Let’s take a look at the details:

{
"type": "webhook_event",
"id": "6b922242-d43b-4213-84da-bb1ed72e7c0b",
"created_at": "2024-05-22T10:01:20-07:00",
"trigger": "SIGN_REQUEST.DECLINED",
"webhook": {
"id": "2770072773",
"type": "webhook"
},
"additional_info": {
"sign_request_id": "7390f653-ffdf-45c3-9d5d-7255b1c0496b",
"signer_emails": [
"barduinor@gmail.com",
"barbasr+signer_b@gmail.com"
],
"external_id": "",
"single_signer_event_email": "barbasr+signer_b@gmail.com",
"single_signer_details": {
"embed_url_external_user_id": "my_id",
"role": "signer",
"order": 1
},
"signer_decision_additional_info": "I refuse to sign this document"
}
}

What if we turn the notifications back on, and send the request to an invalid email?

curl --location 'https://api.box.com/2.0/sign_requests' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YM...Dg' \
--data-raw '{
"parent_folder": {
"id": "249194622181",
"type": "folder"
},
"template_id":"15422517-8adb-42e1-895d-bbe1cf02aafd",
"signers": [
{
"email": "barbasr+signer_b@example.com",
"role": "signer",
"suppress_notifications":false

}
]
}'

We get a signature response from the API and the SIGN_REQUEST.SIGNATURE_REQUESTED from the webhook.

However, a few seconds later we see SIGN_REQUEST.SIGNER_EMAIL_BOUNCED with extra details identifying which signer has the issue.

{
"type": "webhook_event",
"id": "3b0c0931-e226-4251-80f0-cb6dc57a787d",
"created_at": "2024-05-22T10:09:24-07:00",
"trigger": "SIGN_REQUEST.SIGNER_EMAIL_BOUNCED",
"webhook": {
"id": "2770072773",
"type": "webhook"
},
"additional_info": {
"sign_request_id": "bd4ba858-a047-4ff3-b53f-8b9c30b0e5e0",
"signer_emails": [
"barduinor@gmail.com",
"barbasr+signer_b@example.com"
],
"external_id": "",
"single_signer_event_email": "barbasr+signer_b@example.com",
"single_signer_details": {
"embed_url_external_user_id": null,
"role": "signer",
"order": 1
}
}
}

More webhooks

There are still some webhooks that we haven’t triggered yet, because they’re difficult to replicate in a test environment. These include:

  • SIGN_REQUEST.EXPIRED — When time has elapsed beyond the expiration date set in the signature request
  • SIGN_REQUEST.ERROR_FINALIZING — When there’s an internal error in the last steps of the signature request (for example, an error in creating the final signed document)

Final thoughts

Sign-related webhooks, just by themselves, allow your applications to become aware of state changes within your signature requests. As the workflow progresses, your app can react to those changes, improving user awareness across the entire process.

Thoughts? Comments? Feedback?

Drop us a line in our community forum.

--

--