Package: blueink-client-python
blueink-client-pythonA Python client library for the BlueInk eSignature API.OverviewThis README provides a narrative overview of using the Blueink Python client, and
includes examples for many common use cases.Additional resources that might be useful include:Examples atblueink-client-python-examplesrepo on GitHub.The detailedBlueink API Documentation, for
details on the data returned by each API call.For detailed documentation for each method call, please consult the source code,
or rely on auto-complete in your favorite editor / IDE. The code is well commented and
includes Python type annotations, which most IDEs understand.InstallationTo install this library, run the following command:pipinstallblueink-client-pythonBasic UsageRequests to the Blueink API are made via an instance of theblueink.Clientclass. Theblueink.Clientneeds a Blueink private API key. By default the Client looks for
the private key in an environment variable namedBLUEINK_PRIVATE_API_KEY.# In your shell, or in .bashrc or similarexportBLUEINK_PRIVATE_API_KEY=YOUR_PRIVATE_API_KEY_HERE# In your python code, create an instance of the blueink ClientfromblueinkimportClientclient=Client()Setting the private key via the environment is strongly recommended (to avoid
the possiblity of hard-coding your private key in your source code). However, you
can also pass the private key when instantiating a Client, like so:fromblueinkimportClientclient=Client("YOUR_PRIVATE_API_KEY")The Client also has two modes of operations. By default the Client will raise HTTPError
exceptions whenever there's an issue between the client and server (eg. HTTP4xx,
HTTP5xx errors). These come from therequestsmodule. If within your application
you already handle exceptions coming out ofrequeststhen you should be good.
If you setraise_exceptions = Falsethen these  will be returned asNormalizedResponseobjects which are also used for successful communications. See
below for information about these objects.# In your python code, create an instance of the blueink ClientfromblueinkimportClientclient=Client(raise_exceptions=False)Making API CallsMaking API calls with a client instance is easy. For example, to retrieve a list of
Bundles, do:response=client.bundles.list()bundles=response.dataforbundleinbundles:print(bundle.id)The Client class follows a RESTful pattern for making API calls, like so:client.[resource].[method].The supported "resources" are:bundlespersonspacketstemplateswebhooksThe methods correspond to common REST operations:list()retrieve()create()update()delete()However, note that:Not all resources support all methods.Some resources support one-off methods that are unique to that resource.
For example thebundlesresource allows you to retrieve a list of events on
the Bundle by callingclient.bundles.list_events().Detailed documentation for each resource is below.ResponsesAPI calls return aNormalizedResponseinstance. TheNormalizedResponseprovides
the following attributes.response.dataThe json data returned via the API call is accessible via thedataattribute. Thedataattribute supports dictionary access and dot-notation access (for convenience
and less typing).response=client.bundles.retrieve("some bundle ID")bundle_data=response.dataprint(bundle_data['id'])# dictionary-style accessprint(bundle_data.id)# dot notation accessresponse.requestThe request that led to this response. Under-the-hood, the Blueink client library
uses thePython Requests Library.response.requestis an instance ofrequests.PreparedRequest.response.original_responseSimilarly, if you need access to the original response as returned by
the Python Requests library, it's accessible asoriginal_response.response.paginationMost API calls that return a list of data returned paginated results. If so,
information about the pagination is included in thepaginationattribute.Pagination Example:response=client.persons.list()pagination=response.paginationprint(pagination.page_number,' - current page number')print(pagination.total_pages,' - total pages')print(pagination.per_page,' - results per page')print(pagination.total_results,' - total results')See "Requests that Return Lists > Pagination" below.Requests that Return ListsFiltering and SearchingSome BlueinkAPI endpointssupport searching and / or
filtering. In those cases, you can pass querystring parameters to thelist(...)orpaged_list(...)method on those resources.For example:fromblueinkimportClient,constantsclient=Client()# Retrieve Bundles with a status of "Complete"response=client.bundles.list(status=constants.BUNDLE_STATUS.COMPLETE)complete_bundles=response.data# Retrieve Bundles with a status or "Complete" or "Started"statuses=",".join([constants.BUNDLE_STATUS.COMPLETE,constants.BUNDLE_STATUS.STARTED])response=client.bundles.list(status__in=statuses)complete_or_started_bundles=response.data# Retrieve Bundles matching a search of "example.com" (which will match signer email# addresses). We can pass pagination parameters too.response=client.bundles.list(per_page=10,page=2,search="example.com")matching_bundles=response.data# Filtering / searching works with pagination iterators / paged_list() calls as welliterator=client.bundles.paged_list(search="example.com")forpaged_responseiniterator:forbundleinpaged_response.data:print(bundle.id)PaginationBlueink API calls that return a list of results are paginated - ie, if there
are a lot of results, you need to make multiple requests to retrieve all of those
results, including apage_numberparameter (and optionally apage_sizeparameter)
in each request.The details of Blueink pagination scheme can be found in theAPI documentation:This client library provides convenience methods to make looping over
paginated results easier. Whenever there is alist()method available for a resource,
there is a correspondingpaged_list()method available that returns aPaginatedIteratorhelper class to make processing the results easier.You can mostly ignore the details of how thePaginatedIteratorworks. Instead, here
is an example of looping over a paginated set of Bundles:# Loop over all of the Bundles in your account, and print their IDsiterator=client.bundles.paged_list()forpaged_responseiniterator:pg=paged_response.paginationprint(f"Fetched page{pg.page_number}of{pg.total_pages}total pages")forbundleinpaged_response.data:print(bundle.id)Client Method IndexParameters can be found using autocomplete within your IDE. Creates/Updates take a
Python dictionary as the data field, unless special named methods likecreate_from_bundle_helperindicate otherwise. List methods can take query params
as kwargs.Bundle RelatedCreate viaclient.bundles.create(...)orclient.bundles.create_from_bundle_helper(...)List viaclient.bundles.list(...)orclient.bundles.paged_list(...)Retrieve viaclient.bundles.retrieve(...)Cancel viaclient.bundles.cancel(...)List Events viaclient.bundles.list_events(...)List Files viaclient.bundles.list_files(...)List Data viaclient.bundles.list_data(...)Person RelatedCreate viaclient.persons.create(...)orclient.persons.create_from_person_helper(...)List viaclient.persons.list(...)orclient.persons.paged_list(...)Retrieve viaclient.persons.retrieve(...)Delete viaclient.persons.delete(...)Update viaclient.persons.update(...)Packet RelatedUpdate viaclient.packets.update(...)Create Embedded Signing URL viaclient.packets.embed_url(...)Retrieve COE viaclient.packets.retrieve_coe(...)Remind viaclient.packets.remind(...)Template RelatedList viaclient.templates.list(...)orclient.templates.paged_list(...)Retrieve viaclient.templates.retrieve(...)Webhook RelatedWebhook Client MethodsCreate viaclient.webhooks.create(...)List viaclient.webhooks.list(...)Retrieve viaclient.webhooks.retrieve(...)Delete viaclient.webhooks.delete(...)Update viaclient.webhooks.update(...)WebhookExtraHeader Client MethodsCreate viaclient.webhooks.create_header(...)List viaclient.webhooks.list_headers(...)Retrieve viaclient.webhooks.retrieve_header(...)Delete viaclient.webhooks.delete_header(...)Update viaclient.webhooks.update_header(...)WebhookEvent Client MethodsList viaclient.webhooks.list_events(...)Retrieve viaclient.webhooks.retrieve_event(...)WebhookDelivery Client MethodsList viaclient.webhooks.list_deliveries(...)Retrieve viaclient.webhooks.retrieve_delivery(...)Detailed Guide and ExamplesBundlesCreating Bundles with the BundleHelperWhen creating a Bundle via the API, you need to pass quite a bit of data in theclient.bundle.create(...)request. To ease the construction of that data, this
library provides aBundleHelperclass.Below is an example of usingBundleHelperto create a Bundle with 1 document,
and 2 signers. In this example, the uploaded document is specified via a URL.fromblueinkimportBundleHelper,Client,constants# Create a BundleHelper instance, and initialize some basic settings on the Bundlebh=BundleHelper(label="Test Bundle 01",email_subject="Please sign this test bundle",email_message="Here is a test bundle. Please sign it.",is_test=True)# Add a CC recipient, that will receive a copy of the completed docsbh.add_cc("bart.simpson@example.com")# Add a document to the Bundle by providing a publicly accessible URL where# the Blueink platform can download the document to include in the Bundledoc_key=bh.add_document_by_url("https://www.irs.gov/pub/irs-pdf/fw9.pdf")signer1_key=bh.add_signer(name="Homer Simpson",email="homer.simpson@example.com",)signer2_key=bh.add_signer(name="Marge Simpson",email="marge.simpson@example.com",)# Add a field that both signers can editbh.add_field(document_key=doc_key,x=1,y=15,w=60,h=20,p=3,kind=constants.FIELD_KIND.INPUT,label="Please enter some text",editors=[signer1_key,signer2_key])# Add a signature field for signer1bh.add_field(document_key=doc_key,x=1,y=15,w=68,h=30,p=4,kind=constants.FIELD_KIND.ESIGNATURE,label="Sign Here",editors=[signer1_key])client=Client()response=client.bundles.create_from_bundle_helper(bh)print(f"Status:{response.status}. Created bundle with ID{response.data.id}")Using theBundleHelper, you can add files to a Bundle in multiple ways:bh=BundleHelper(...)# 0) Add a document using a URL to a web resource:doc0_key=bh.add_document_by_url("https://www.example.com/example.pdf")# 1) Add a document using a path to the file in the local filesystemdoc1_key=bh.add_document_by_path("/path/to/file/example.pdf")# 2) Add a document using a UTF-8 encoded Base64 string:filename,pdf_b64=read_a_file_into_b64_string()doc02_key=bh.add_document_by_b64(filename,pdf_b64)# 3) Add a document that you have already read into a Python bytearray objectfilename,pdf_bytearray=read_a_file_into_bytearray()doc03_key=bh.add_document_by_bytearray(filename,pdf_bytearray)# 4) Add a document as a File object. Make sure to use 'with' or suitably close the file#    after creating the document.withopen("/path/to/file/example.pdf",'rb')asfile:doc04_key=bh.add_document_by_file(file)RetrievalGetting a single bundle is fairly easy. They can be accessed with a single call. To get
the additional data (events, files, data), set the related_data flag to True.response=client.bundles.retrieve(bundle_id,related_data=True)bundle=response.databundle_id=bundle.id# additional data fields (only exist if related_data==True)events=bundle.eventsfiles=bundle.filesdata=bundle.dataListingListing has several options regarding pagination. You can also choose to append the
additional data on each retrieved
bundle as you can with single fetches.client.bundles.paged_list()returns an
iterator object that lazy loads
subsequent pages. If no parameters are set, it will start at page 0 and have up to 50
bundles per page.# EXAMPLE: Collecting all bundle IDsids=[]forapi_callinclient.bundles.paged_list(start_page=1,per_page=5,related_data=True):print(f"Paged Call:{api_call.data}")forbundleinapi_call.data:ids.append(bundle.id)PersonsCreating a person is similar to a creating a Bundle. There is a PersonHelper to help
create a personfromcopyimportdeepcopyfromrequests.exceptionsimportHTTPErrorfrompprintimportpprintfromblueink.clientimportClientfromblueink.person_helperimportPersonHelperclient=Client()ph=PersonHelper()# Try and create a person without setting anything up#  this is expected to errortry:result=client.persons.create_from_person_helper(ph)exceptHTTPErrorase:print(e)pprint(e.response.text)exceptExceptionase:print("Error:")print(e)# Make up some metadata to add to the personmetadata={}metadata["number"]=1metadata["string"]="stringy"metadata["dict"]={}metadata["dict"]["number"]=2metadata["list"]=[]metadata["list"].append(3)# Set the metadata of the personph.set_metadata(metadata)# Set the persons nameph.set_name("New Name")# Add email contacts for the personph.add_email("test@email.com")ph.add_email("test2@email.com")ph.add_email("test3@email.com")# Get all of the emails for the personall_current_emails=ph.get_emails()# Remove an email from the listall_current_emails.remove("test@email.com")# Overwrite the existing email list with this new list#   Effectively removing test@email.com listph.set_emails(all_current_emails)# Add phone number contact for the personph.add_phone("5055551212")ph.add_phone("5055551213")ph.add_phone("5055551214")# Get all of the phone numbers for the personall_current_phones=ph.get_phones()# Remove a phone number from the listall_current_phones.pop()# Overwrite the existing email list with this new list#   Effectively removing last phone numberph.set_phones(all_current_phones)# Create the person and check the resulttry:result=client.persons.create_from_person_helper(ph)pprint(f"Result Create:{result.status}:{result.data}")exceptHTTPErrorase:print(e)pprint(e.response.text)exceptExceptionase:print("Error:")print(e)# Change the persons name and call updateresult.data.name="Second Name""""The channels in the response include both email and phoneIf we want to update with this data we need to remove the onesthat are blank"""new_channels=[]forchannelinresult.data.channels:new_channel=deepcopy(channel)forkey,valueinchannel.items():# Remove the key/value pairs that are not validifnotvalue:new_channel.pop(key)new_channels.append(new_channel)# Set the channels to the recreated channels without the invalid keysresult.data.channels=new_channelstry:result=client.persons.update(result.data.id,result.data)pprint(f"Result Update:{result.status}:{result.data}")exceptHTTPErrorase:print(e)pprint(e.response.text)exceptExceptionase:print("Error:")print(e)# Retrieve the persontry:result=client.persons.retrieve(result.data.id)pprint(f"Result Retrieve:{result.status}:{result.data}")exceptHTTPErrorase:print(e)pprint(e.response.text)exceptExceptionase:print("Error:")print(e)# Perform a partial update to change the name againthird_name={"name":"Third Name"}try:result=client.persons.update(result.data.id,third_name,True)pprint(f"Result Partial Update:{result.status}:{result.data}")exceptHTTPErrorase:print(e)pprint(e.response.text)exceptExceptionase:print("Error:")print(e)# Delete the person from your account and check the resulttry:result=client.persons.delete(result.data.id)pprint(f"Result Delete:{result.status}:{result.data}")exceptHTTPErrorase:print(e)pprint(e.response.text)exceptExceptionase:print("Error:")print(e)"""Create a person and pass extra argumentsif using a older version of sdk that doesn'tsupport certain new API parameters you can add themthis way in the person helperIf calling another method that just takes a dictadd them to the dict directly"""try:ph=PersonHelper(name="New Person")result=client.persons.create_from_person_helper(ph,hidden_person=True)pprint(f"Result Create With Extra Args:{result.status}:{result.data}")result=client.persons.delete(result.data.id)pprint(f"Result Delete:{result.status}:{result.data}")exceptHTTPErrorase:print(e)pprint(e.response.text)exceptExceptionase:print("Error:")print(e)PacketsPackets can be updated. Reminders may be triggered, and COEs can be retrieve using the
client:# Retrieveclient.packets.retrieve(packet_id)# Updateclient.packets.update(packet_id,packet_json)# Remindclient.packets.remind(packet_id)# Get COEclient.packets.retrieve_coe(packet_id)TemplatesTemplates can be listed (non-paged), listed (paged) or retrieved singly:# non pagedtemplates_list_response=client.templates.list()# pagedforpageinclient.templates.paged_list():page.data# templates in page# singletemplate_response=client.templates.retrieve(template_id)WebhooksWebhooks can be interacted with via several methods. Webhooks also have related objects, such asWebhookExtraHeaders,WebhookEvents, andWebhookDeliverieswhich have their own
methods to interact with.fromcopyimportdeepcopyfromrequestsimportHTTPErrorfromsrc.blueinkimportClientfromsrc.blueink.constantsimportEVENT_TYPEWEBHOOK_01={"url":"https://www.example.com/01/","enabled":True,"json":True,"event_types":[EVENT_TYPE.EVENT_BUNDLE_LAUNCHED,EVENT_TYPE.EVENT_BUNDLE_COMPLETE,]}WEBHOOK_01_UPDATE={"enabled":False,"event_types":[EVENT_TYPE.EVENT_PACKET_VIEWED]}WEBHOOK_01_EXTRA_HEADER_A={"name":"Courage_The_Cowardly_Webhook","value":"Muriel Bagge","order":0,}client=Client()# --------------# Attempt posting a new Webhook# --------------try:create_resp=client.webhooks.create(data=WEBHOOK_01)webhook=create_resp.dataprint(f"Created webhook{webhook.id}")exceptHTTPErrorase:print("Error Creating Webhook: ")print(e)# --------------# Update Webhook# --------------try:update_resp=client.webhooks.update(webhook.id,WEBHOOK_01_UPDATE)webhook=update_resp.dataprint(f"Updated webhook{webhook.id}")exceptHTTPErrorase:print("Error Updating Webhook: ")print(e)# --------------# Create and add an ExtraHeader to the above Webhook# --------------extra_header_data=deepcopy(WEBHOOK_01_EXTRA_HEADER_A)extra_header_data["webhook"]=webhook.idtry:header_create_resp=client.webhooks.create_header(data=extra_header_data)header=header_create_resp.dataprint(f"Added ExtraHeader{header}to{webhook.id}")exceptHTTPErrorase:print("Error Creating ExtraHeader: ")print(e)# --------------# List Webhooks# --------------try:list_resp=client.webhooks.list()webhook_list=list_resp.dataprint(f"Found{len(webhook_list)}Webhooks")forwhinwebhook_list:print(f" - Webhook ID:{wh.id}to{wh.url}")exceptHTTPErrorase:print("Error Listing Webhooks: ")print(e)# --------------# Delete webhook# --------------try:delete_resp=client.webhooks.delete(webhook.id)print(f"Deleted Webhook{webhook.id}")exceptHTTPErrorase:print(f"Error Deleting Webhooks{webhook.id}")print(e)