blueink / blueink-client-php
A PHP client library for the BlueInk eSignature REST API
Requires
- php: >=8.1
- guzzlehttp/guzzle: ^7.8
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10
This package is auto-updated.
Last update: 2026-05-09 19:44:19 UTC
README
A PHP SDK for the Blueink eSignature REST API. For full API reference see the Blueink API v2 docs.
Requirements
- PHP 8.1 or newer
- ext-json
- Guzzle 7.x (installed as a dependency)
Installation
composer require blueink/blueink-client-php
Quickstart
use Blueink\ClientSDK\Client; // Provide the key directly... $client = new Client('<YOUR_PRIVATE_API_KEY>'); // ...or set BLUEINK_PRIVATE_API_KEY in the environment and call: // $client = new Client(); $response = $client->bundles->list(); foreach ($response->data as $bundle) { echo $bundle['id'] . "\n"; }
Configuration
The Client constructor accepts:
| Argument | Default | Description |
|---|---|---|
$private_api_key |
getenv('BLUEINK_PRIVATE_API_KEY') |
Blueink private API key. |
$base_url |
getenv('BLUEINK_API_URL') ?? https://api.blueink.com/api/v2 |
Override for sandbox or custom hosts. |
$raise_exceptions |
true |
When false, 4XX/5XX responses are returned as NormalizedResponse instead of throwing a Guzzle exception. |
$client = new Client( private_api_key: 'sk_live_…', base_url: 'https://sandbox.blueink.com/api/v2', raise_exceptions: false, );
Responses: NormalizedResponse
Every subclient method returns a Blueink\ClientSDK\NormalizedResponse:
$response = $client->bundles->retrieve('abc123'); $response->status; // int HTTP status code $response->data; // mixed Decoded JSON body (associative array) or raw string $response->headers; // array<string,string> Response headers (last value per name) $response->pagination; // ?Pagination Parsed X-Blueink-Pagination header (list endpoints) $response->originalResponse; // Psr\Http\Message\ResponseInterface
The most-recent response for a given subclient is also retained:
$last = $client->bundles->getLastResponse();
Subclients
The Client exposes one property per resource:
| Property | Class | Resource |
|---|---|---|
$client->bundles |
BundleSubClient |
Bundles (envelopes) |
$client->persons |
PersonSubClient |
Persons (signers / contacts) |
$client->packets |
PacketSubClient |
Packets (per-recipient) |
$client->templates |
TemplateSubClient |
Document templates |
$client->envelope_templates |
EnvelopeTemplateSubClient |
Envelope templates |
$client->webhooks |
WebhookSubClient |
Webhooks, headers, events, deliveries |
Bundles
Create a Bundle from a hand-built payload:
$response = $client->bundles->create([ 'label' => 'A Test Bundle', 'is_test' => true, 'packets' => [[ 'key' => 'signer-1', 'name' => 'Peter Gibbons', 'email' => 'peter.gibbons@example.com', ]], 'documents' => [[ 'key' => 'doc-01', 'file_url' => 'https://example.com/contract.pdf', 'fields' => [], ]], ]); $bundle_id = $response->data['id'];
Or use BundleHelper to assemble it. The helper exposes convenience methods
for adding signers, documents, fields, and auto-placements without hand-rolling
the nested payload:
use Blueink\ClientSDK\BundleHelper; $bundle = new BundleHelper([ 'label' => 'A Test Bundle', 'email_subject' => 'Please sign this test bundle', 'is_test' => true, ]); // Add signers (each requires email or phone). Returns the generated packet key. $signer1 = $bundle->addSigner(name: 'Peter Gibbons', email: 'peter@example.com'); $signer2 = $bundle->addSigner(name: 'Bill Lumbergh', email: 'bill@example.com'); // Add a Document by URL, base64, file path (read at build time), file path // (streamed as multipart at request time), or raw HTML. $doc_key = $bundle->addDocumentByURL('https://example.com/contract.pdf'); // $bundle->addDocumentByPath('/tmp/nda.pdf'); // $bundle->addDocumentByFile('/tmp/large.pdf', 'application/pdf'); // $bundle->addDocumentByHTML('<h1>Statement of Work</h1>...'); // Add a field at fixed coordinates, scoped to a list of editor packet keys. $bundle->addField( document_key: $doc_key, x: 1, y: 15, w: 60, h: 6, p: 1, kind: 'inp', editors: [$signer1, $signer2], label: 'Full name', ); // Or let the API auto-place a field by searching the document text. $bundle->addAutoPlacement( document_key: $doc_key, kind: 'sig', search: 'Signature:', w: 30, h: 6, offset_x: 8, offset_y: -1, editors: [$signer1], ); $response = $client->bundles->createFromBundleHelper($bundle);
When the helper has files queued via addDocumentByFile(), the SDK transparently
switches the request to multipart/form-data.
Building a Bundle from a document Template
addDocumentTemplate() adds a Document backed by an existing template, with
optional role assignments and initial field values. assignRole() and
setValue() append to the same Document after the fact:
$signer = $bundle->addSigner( name: 'Peter Gibbons', email: 'peter@example.com', key: 'signer-1', ); $tmpl_key = $bundle->addDocumentTemplate( template_id: 'tmpl_abc123', assignments: ['signer' => 'signer-1'], // role => signer key initial_field_values: ['agree' => true], // field key => value ); // Equivalent, post-hoc: $bundle->assignRole($tmpl_key, 'signer-1', 'witness'); $bundle->setValue($tmpl_key, 'effective_date', '2026-01-01'); $response = $client->bundles->createFromBundleHelper($bundle);
Other Bundle operations:
$client->bundles->retrieve($bundle_id); $client->bundles->retrieve($bundle_id, related_data: true); // attach events / files / data $client->bundles->cancel($bundle_id); $client->bundles->listEvents($bundle_id); $client->bundles->listFiles($bundle_id); $client->bundles->listData($bundle_id);
Persons
use Blueink\ClientSDK\PersonHelper; $helper = new PersonHelper(['name' => 'Jane Doe']); $helper->addEmail('jane@example.com'); $helper->addPhone('+15551234567'); $created = $client->persons->createFromPersonHelper($helper); $person_id = $created->data['id']; $client->persons->retrieve($person_id); $client->persons->update($person_id, ['metadata' => ['vip' => true]], partial: true); $client->persons->delete($person_id);
The Blueink API normalizes
nameby splitting on whitespace into first/last tokens, so the round-trippednamemay not be byte-identical to what was sent.
Packets
$client->packets->update($packet_id, ['email' => 'new@example.com']); $client->packets->embedURL($packet_id); // signed embedded-signing URL $client->packets->retrieveCOE($packet_id); // Certificate of Evidence $client->packets->remind($packet_id);
Templates
$client->templates->list(); $client->templates->retrieve($template_id);
Envelope Templates
Envelope templates are reusable, end-to-end Bundle workflows configured in the Blueink dashboard. The subclient is read-only:
$client->envelope_templates->list(); $client->envelope_templates->retrieve($envelope_template_id); foreach ($client->envelope_templates->pagedList(per_page: 100) as $page) { foreach ($page->data as $tpl) { echo $tpl['id'] . "\n"; } }
To create a Bundle from an envelope template, configure a BundleHelper with
setEnvelopeTemplate() (plus any signers and field overrides) and post it via
createFromEnvelopeTemplateHelper():
use Blueink\ClientSDK\BundleHelper; $bundle = new BundleHelper([ 'label' => 'Onboarding paperwork', 'is_test' => true, ]); $bundle->addSigner(name: 'Peter Gibbons', email: 'peter@example.com', key: 'signer-1'); $bundle->setEnvelopeTemplate( template_id: 'env_tmpl_abc123', field_values: ['company_name' => 'ACME Corp'], ); $bundle->addEnvelopeTemplateFieldValue('start_date', '2026-01-15'); $response = $client->bundles->createFromEnvelopeTemplateHelper($bundle);
For pre-built payloads, createFromEnvelopeTemplate(array $data) posts the
array directly to /bundles/create_from_envelope_template/.
Webhooks
$created = $client->webhooks->create([ 'name' => 'My integration', 'url' => 'https://example.com/blueink-webhook', 'event_types' => ['bundle_complete'], ]); $client->webhooks->update($id, ['url' => '…'], partial: true); $client->webhooks->delete($id); // Custom request headers Blueink will send to your endpoint $client->webhooks->createHeader([ 'webhook' => $id, 'name' => 'X-My-Token', 'value' => 'shhh', 'order' => 0, ]); // Verification of incoming deliveries $client->webhooks->retrieveSecret(); $client->webhooks->regenerateSecret(); // Inspection $client->webhooks->listEvents(); $client->webhooks->listDeliveries(); $client->webhooks->retrieveDelivery($delivery_id);
Pagination
List endpoints accept page and per_page and return a Pagination object on
the response, parsed from the X-Blueink-Pagination header:
$response = $client->bundles->list(page: 1, per_page: 25); $response->pagination->page_number; $response->pagination->total_pages; $response->pagination->per_page; $response->pagination->total_results;
For automatic page-walking, every list-capable subclient exposes a pagedList()
that returns a Paginated iterator. Each iteration yields the next page's
NormalizedResponse:
foreach ($client->bundles->pagedList(per_page: 100) as $page) { foreach ($page->data as $bundle) { // … } }
Error handling
By default, 4XX/5XX responses raise a Blueink\ClientSDK\BlueinkApiError,
a typed exception that parses the Blueink error body shape:
{
"detail": "Invalid input.",
"code": "invalid",
"errors": [
{ "field": "channels", "message": "This field is required." }
]
}
use Blueink\ClientSDK\BlueinkApiError; try { $client->persons->create(['name' => 'Jane Doe']); } catch (BlueinkApiError $e) { $e->status_code; // int HTTP status (also $e->getCode()) $e->detail; // ?string body.detail $e->api_code; // ?string body.code (named api_code to avoid clashing with getCode()) $e->errors; // array<int,array{field?:string,message?:string}> $e->body; // decoded body (array) or raw string when not JSON $e->response; // ?Psr\Http\Message\ResponseInterface $e->request; // ?Psr\Http\Message\RequestInterface $e->getPrevious(); // GuzzleHttp\Exception\BadResponseException foreach ($e->errors as $error) { printf("%s: %s\n", $error['field'] ?? '_', $error['message'] ?? ''); } }
BlueinkApiError extends \RuntimeException, so a generic catch (\Throwable $e)
still works. The original Guzzle BadResponseException (ClientException /
ServerException) is preserved as the previous exception. Network-level
failures (GuzzleHttp\Exception\ConnectException, RequestException without a
response) are not wrapped and continue to bubble up unchanged.
To inspect failures without try/catch, construct the client with
raise_exceptions: false. 4XX and 5XX responses then come back as
NormalizedResponse objects with status and decoded data:
$client = new Client(raise_exceptions: false); $response = $client->persons->create(['name' => 'Jane Doe']); if ($response->status >= 400) { var_dump($response->data); }
Testing
The SDK ships with two PHPUnit suites:
unit(default) — fast, hermetic. Uses Guzzle'sMockHandlerto verify request shaping (verb, URL, headers, JSON / multipart body) without touching the network.integration— opt-in. Hits a real Blueink environment using the key inBLUEINK_PRIVATE_API_KEY(and optionalBLUEINK_API_URL). Tests are skipped automatically when the key is absent. Use a sandbox account.
# Unit suite (runs by default) ./vendor/bin/phpunit # Integration suite, against your sandbox BLUEINK_PRIVATE_API_KEY=sk_sandbox_… \ BLUEINK_API_URL=https://sandbox.blueink.com/api/v2 \ ./vendor/bin/phpunit --testsuite=integration
License
MIT.