signingstudio/signingstudio-php

Official PHP SDK for the Signing Studio e-signature API.

Maintainers

Package info

github.com/alyasdds/signingstudio-php

Homepage

Issues

Documentation

pkg:composer/signingstudio/signingstudio-php

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

dev-main 2026-07-22 12:59 UTC

This package is not auto-updated.

Last update: 2026-07-23 14:15:34 UTC


README

CI Latest Stable Version License PHP Version

Official PHP client for the Signing Studio e-signature API. Covers every public v1 endpoint — send documents from templates, poll signing progress, manage templates and their fields, and verify webhook deliveries.

Install

composer require signingstudio/signingstudio-php

Requires PHP 8.1+ and the json + hash extensions (both ship by default).

Quick start

use SigningStudio\Client;

$client = new Client('sk_live_your_api_key');

$doc = $client->documents()->send([
    'template_id' => '11111111-2222-3333-4444-555555555555',
    'title'       => 'MSA — Acme',
    'recipients'  => [
        ['name' => 'Alex Doe', 'email' => 'alex@acme.com'],
    ],
]);

echo "Sent {$doc['id']} ({$doc['status']})\n";

Get your API key from Signing Studio → Settings → API Keys. The plaintext key is shown once at creation — store it in a secrets manager, never in source control.

Configuration

The one-arg constructor is enough for most callers. Pass a Config when you need to override the base URL (staging), retry ceiling, or timeouts:

use SigningStudio\Client;
use SigningStudio\Config;

$client = new Client(new Config(
    apiKey: 'sk_live_...',
    baseUrl: 'https://api.signingstudio.com',   // default
    maxRetries: 3,                              // 429 + 5xx + network errors
    connectTimeout: 10,                         // seconds
    requestTimeout: 120,                        // seconds
    userAgent: 'my-app/1.4',
));

The retry policy is intentionally conservative:

  • 429 with Retry-After ≤ 60s → sleep and retry, up to maxRetries.
  • 429 with Retry-After > 60s (typically a daily quota) → surface RateLimitException immediately.
  • 5xx → exponential backoff (500ms · 1s · 2s · 4s · …) with jitter, up to maxRetries.
  • Network errors (connect fail, read timeout) → same backoff as 5xx.

Documents

// List
$list = $client->documents()->list([
    'status' => 'sent',
    'view'   => 'active',   // 'archived' | 'deleted' | 'all'
    'limit'  => 50,
    'offset' => 0,
]);

// Send from a template
$doc = $client->documents()->send([
    'template_id'  => $templateId,
    'title'        => 'MSA — Acme',
    'subject'      => 'Please sign',                // optional email subject
    'message'      => 'Signing at your convenience',
    'expires_at'   => '2026-08-01T00:00:00Z',       // optional
    'recipients'   => [
        ['name' => 'Alex', 'email' => 'alex@acme.com', 'signing_order' => 0],
        ['name' => 'Bo',   'email' => 'bo@acme.com',   'signing_order' => 1],
    ],
    'prefill_values' => [                           // optional
        ['field_name' => 'company', 'value' => 'Acme Inc.'],
    ],
]);

// Read
$doc = $client->documents()->get($id);
$progress = $client->documents()->progress($id);  // cheap; ideal for polling
$activity = $client->documents()->activity($id);  // full audit log

// Actions
$client->documents()->cancel($id);
$client->documents()->archive($id);
$client->documents()->unarchive($id);
$client->documents()->restore($id);
$client->documents()->delete($id);                // soft
$client->documents()->delete($id, hard: true);    // hard purge (must be cancelled/completed/declined/expired)

// Reminder — mints a fresh signing URL for a specific recipient.
$out = $client->documents()->remind($documentId, $recipientId);
$fresh = $out['signing_url'];

// Download
$url = $client->documents()->downloadUrl($id)['url']; // presigned; ~1h TTL
$bytes = $client->documents()->downloadPdf($id);       // raw bytes

Send-payload rules the server enforces

  • template_id is required; ad-hoc PDF sends without a template are not exposed on v1.
  • recipients must have at least one entry. Sequential signing runs in signing_order.
  • prefill_values[] must have either field_id (uuid) or a non-empty field_name slug plus a value.
  • Signature / initials fields cannot be prefilled — 422 if you try.
  • Any template field that is both required: true and readonly: true MUST be prefilled — 422 with a list of missing labels.
  • Sends count against the account's monthly documents_per_month quota — 429 when tripped.

Templates

// List active templates
$templates = $client->templates()->list();

// Create from a PDF (path, resource, or raw bytes all accepted)
$template = $client->templates()->create(
    __DIR__ . '/msa.pdf',
    [
        'name'             => 'MSA v2',
        'signer_count'     => 1,
        'delivery_methods' => ['email'],
        'signers'          => [
            ['role' => 'Customer', 'delivery' => ['email']],
        ],
    ],
);

// Get / partial update / soft-delete
$template = $client->templates()->get($id);
$client->templates()->update($id, ['name' => 'MSA v3']);
$client->templates()->delete($id);

// PDF versioning — replace and history
$client->templates()->replacePdf($id, __DIR__ . '/msa-updated.pdf');
$versions = $client->templates()->history($id);
$oldPdfUrl = $client->templates()->historyPdfUrl($id, $versions[0]['id'])['url'];

// Fields — full replace
$client->templates()->setFields($id, [
    [
        'field_type'   => 'signature',
        'page'         => 1,
        'x' => 60, 'y' => 82, 'width' => 30, 'height' => 6,
        'signer_index' => 0,
        'required'     => true,
    ],
    [
        'field_type'   => 'text',
        'page'         => 1,
        'x' => 10, 'y' => 20, 'width' => 30, 'height' => 4,
        'signer_index' => 0,
        'name'         => 'company',
        'label'        => 'Company name',
        'required'     => true,
    ],
]);

// Duplicate + archive
$copy = $client->templates()->duplicate($id);
$client->templates()->archive($id);

PDF upload constraints

  • Max 50 MB per file.
  • application/pdf only — other content types 400.
  • The multipart form field name must be file (the SDK sets this for you).
  • Passing a string that looks like a filesystem path opens it with fopen; anything else is treated as raw PDF bytes.

Webhooks

Verify incoming deliveries against your webhook's secret before trusting the payload:

use SigningStudio\Client;

$verifier = Client::webhookVerifier('whs_your_secret');

$raw = file_get_contents('php://input') ?: '';
$sig = $_SERVER['HTTP_X_DDS_SIGNATURE'] ?? '';

if (!$verifier->isValid($raw, $sig)) {
    http_response_code(401);
    exit;
}

$payload = json_decode($raw, true);
// $payload['event'] is one of:
//   document.sent | document.viewed | document.signed | document.declined | document.completed

Payload shape

{
  "event": "document.signed",
  "data": {
    "document": {
      "id": "uuid",
      "title": "string",
      "status": "sent|viewed|completed|declined|cancelled|expired",
      "template_id": "uuid|null",
      "sent_at": "…|null",
      "completed_at": "…|null"
    },
    "download_url": "https://…|null",
    "download_expires_in_seconds": 3600,
    "recipients": [ /* …with viewed_at/signed_at/declined_at */ ],
    // Event-specific:
    //   document.viewed / .signed → { "recipient": {…}, "ip": "…"|null }
    //   document.declined         → { "recipient": {…}, "reason": "…"|null, "ip": "…"|null }
    //   document.completed        → { "completed_by": {…} }
  },
  "timestamp": "2026-07-20T14:32:11Z"
}

Always sign the RAW body, not a parsed-and-re-encoded body. Any whitespace shift breaks the HMAC.

Errors

use SigningStudio\Exception\{
    SigningStudioException,
    ApiException,
    AuthenticationException,
    NotFoundException,
    ValidationException,
    RateLimitException,
};

try {
    $client->documents()->send([...]);
} catch (ValidationException $e) {
    foreach ($e->getErrors() as $field => $messages) { /* … */ }
} catch (RateLimitException $e) {
    // $e->getWindow() === 'minute' | 'day' | null (quota)
    // $e->getRetryAfter() === int|null
} catch (AuthenticationException $e) {
    // Refresh the API key.
} catch (NotFoundException $e) {
    // The resource does not exist (or belongs to another tenant).
} catch (ApiException $e) {
    // Everything else that came back with a non-2xx HTTP status.
    error_log("request={$e->getRequestId()} status={$e->getStatusCode()}");
}

All SDK-thrown exceptions inherit from SigningStudioException.

Rate limits

Every response carries:

  • X-RateLimit-Limit-Minute, X-RateLimit-Remaining-Minute
  • X-RateLimit-Limit-Day, X-RateLimit-Remaining-Day

Response::getRateLimitRemainingMinute() / getRateLimitRemainingDay() expose the remaining counts:

$response = $client->documents()->list([...]);
// Response object isn't returned by the resource methods directly today —
// they return the parsed data array. The rate-limit headers are still
// received by the transport; if you need them, drop down to the HttpClient.

Platform defaults are 120 requests/minute and 20,000 requests/day per API key. Per-account and per-key overrides can raise or lower these.

Testing

composer install
composer test           # phpunit
composer analyse        # phpstan level 7
composer cs-check       # php-cs-fixer dry-run

CI runs against PHP 8.1, 8.2, 8.3, 8.4 on every push/PR.

Versioning

Semantic versioning. Breaking changes bump the major; new endpoints or fields bump the minor; bug fixes bump the patch. CHANGELOG.md records every release.

License

MIT. See LICENSE.