Exporting Adobe Sign Agreement PDFs
Adobe Sign/Echo Sign does not give you a way to export multiple PDFs, leaving the user to have to manually download each agreement one-by-one.
I am having to export 10+ years worth of PDFs, so here is the way I am doing it programmatically.
The idea is to write a script that fetches a list of all agreements, and then downloads each agreement PDF.
First we figure out the endpoint URLs that the website uses to list out agreements.
Adobe Sign only shows 100 agreements at a time.
Open the the Network tab, filtering by "Fetch/XHR".
Each time you trigger a listing (scrolling down to trigger the next page, clicking on a different status in the sidebar), we can see a call to search
is made.
Select the most recent search
call to see its details.
Take a look at the Reponse tab to see if this is the request for returning the list of agreements:
A full list of agreements (max. 100) is about 6k lines of JSON.
Now that we know this specific request is what gets us the list of agreements, we can move forward with trying to emulate a search ourselves.
The information we want is the Request URL, the Authorization token, and the payload.
The Headers tab shows the Request URL and Authorization data:
And the Payload tab shows our search parameters in the form of JSON:
Before we write up anything, we're going to quickly test it on Postman to make sure we can make these calls from outside Adobe Sign -
In Postman:
- Set method to POST
- Set URL to the Request URL
- Set Authorization to "Bearer", and paste in the long token into the text box
- Set Body to "raw", and paste in the JSON from the payload tab
Submitting this will return the same list of results we saw in the Response tab of Adobe Sign's Network tab.
Now we can try to make a request to download a PDF.
On Adobe Sign, with the Network tab still open, select an agreement and click on "Download PDF".
This triggers a request to "combinedDocument":
We do the same here, with retrieving the Request URL and Authorization token - no Payload needed (it's included in the URL this time).
Now make the same GET request on Postman using the Request URL and Authorization token.
The file will be returned:
Now we have all the information we need to write the script in our preferred language.
For those with patrician tastes, this preferred language would be PHP, using CodeIgniter 3.
As a prep when doing these batch actions, I like to store all the data in a table to keep track of progress like which files have been downloaded.
To store this info in this way is also useful in searching agreements by date, participants, name, etc.
Here is the table create code which includes all fields returned by Adobe Sign:
CREATE TABLE `adobe_sign_agreements` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`agreement_id` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`name` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`group_id_set` TEXT NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`note` TEXT NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`workflow_id` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`has_unresolved_bounce` INT(11) NULL DEFAULT NULL,
`user_organization` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`user_name` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`active_participant_org` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`roles` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`reference_url` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`is_soft_deleted` INT(11) NULL DEFAULT NULL,
`participation_count` INT(11) NULL DEFAULT NULL,
`participation_completion_count` INT(11) NULL DEFAULT NULL,
`agreement_type` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`sharer` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`agreement_sub_types` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`agreement_parent_id` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`state` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`create_date` TIMESTAMP NULL DEFAULT NULL,
`termination_dates` TEXT NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`active_participant` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`participation_completion_percentage` DOUBLE NULL DEFAULT NULL,
`sharer_organization` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`termination_dates_confirmed` INT(11) NULL DEFAULT NULL,
`participation_set_names` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`last_transaction_date` TIMESTAMP NULL DEFAULT NULL,
`active_reminder_count` INT(11) NULL DEFAULT NULL,
`expire_date` TIMESTAMP NULL DEFAULT NULL,
`sender_organization` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`user_id` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`sender` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`group_id` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`organization_id` VARCHAR(255) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`is_password_protected` INT(11) NULL DEFAULT NULL,
`completed_reminder_count` INT(11) NULL DEFAULT NULL,
`modify_date` TIMESTAMP NULL DEFAULT NULL,
`participant_list` TEXT NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
`downloaded` INT(11) NOT NULL DEFAULT '0',
`created` TIMESTAMP NULL DEFAULT current_timestamp(),
`modified` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `agreement_id` (`agreement_id`) USING BTREE
)
COLLATE='latin1_swedish_ci'
ENGINE=InnoDB
AUTO_INCREMENT=848
;
There are additional columns id
, created
, modified
, downloaded
.
I always like to keep internal date information for record creation and modification dates.
You should too.
The downloaded
bool is to keep track of which files have been downloaded.
Here was my function to call a search to get a list of agreements:
public function search($offset = 0) { $ch = curl_init(); $url = 'https://[YOUR ACCOUNT URL].echosign.com/api/gateway/search'; curl_setopt($ch, CURLOPT_URL, $url); $authorization = 'Authorization:Bearer ' . [YOUR ADOBE SIGN AUTHORIZATION BEARER TOKEN]; curl_setopt($ch, CURLOPT_HTTPHEADER, array($authorization, 'Content-Type: application/json')); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); $body = '[THE PLAINTEXT JSON PAYLOAD FROM THE NETWORK PAYLOAD TAB WE LOOKED AT]'; curl_setopt($ch, CURLOPT_POSTFIELDS, $body); curl_close($ch); $result = json_decode($result); return $result->result_sets[0]->items; }
And the function to download all files from the table:
public function import_pdfs() { $this->db->where('downloaded', 0); $this->db->from('adobe_sign_agreements'); $agreements = $this->db->get()->result(); foreach ($agreements as $agreement) { sleep(2); $output_filename = FCPATH . '/adobe_sign_pdfs/' . date('Y-m-d', strtotime($agreement->create_date)) . '-' . $agreement->name . '.pdf'; $host = "https://[YOUR ACCOUNT URL].echosign.com/api/rest/v6/agreements/{$agreement->agreement_id}/combinedDocument?attachAuditReport=true"; $ch = curl_init(); $authorization = 'Authorization:Bearer ' . [YOUR ADOBE SIGN AUTHORIZATION BEARER TOKEN]; curl_setopt($ch, CURLOPT_HTTPHEADER, array($authorization)); curl_setopt($ch, CURLOPT_URL, $host); curl_setopt($ch, CURLOPT_VERBOSE, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $result = curl_exec($ch); $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $fp = fopen($output_filename, 'wb'); fwrite($fp, $result); fclose($fp); if (!empty($result) && $http_status == '200') { $this->db->set('downloaded', 1); $this->db->where('id', $agreement->id); $this->db->update('adobe_sign_agreements'); } } }
I have a third function which would call the search
function in a loop until the search returns no more agreements.
The third function is also the one that stores the individual agreement data in the table.
One difference I found is that on Adobe Sign, the Download PDF request returns a PDF with an extra page for the changelog of the agreement.
I wasn't able to figure out why that page is missing in our Postman/script version of the PDF.
In any case, if you need that information, you can do the same process as above, but with clicking the "Activity" link in the sidebar instead of "Download PDF".
You will see that it creates a request for event
which returns the changelog in JSON.
Doing this was fun, it reminded me of CheatEngine.
The above steps would work for extracting data out of most Javascript-heavy platforms.