Интеграция электронной подписи PandaDoc на сайт
PandaDoc отличается от DocuSign и SignNow тем, что это не просто сервис подписи, а полноценная платформа для работы с документами: создание из шаблонов, переменные, approval workflows, платёжные блоки внутри документа, аналитика открытий. REST API покрывает весь цикл от создания до архивирования.
Регистрация приложения и аутентификация
В PandaDoc Developer Dashboard: создать приложение → получить Client ID и Client Secret. Два режима аутентификации:
- API Key — простой ключ в заголовке, для серверных интеграций без пользовательского контекста
- OAuth 2.0 — для многопользовательских приложений
// Самый простой вариант для собственного сайта
$headers = [
'Authorization' => 'API-Key ' . config('services.pandadoc.api_key'),
'Content-Type' => 'application/json',
];
Для OAuth — стандартный Authorization Code Flow на app.pandadoc.com/oauth2/authorize.
Создание документа из шаблона
PandaDoc поддерживает шаблоны — это удобнее, чем каждый раз загружать PDF:
class PandaDocService
{
private string $baseUrl = 'https://api.pandadoc.com/public/v1';
public function createFromTemplate(
string $templateId,
array $recipient,
array $tokens
): array {
$response = Http::withHeaders([
'Authorization' => 'API-Key ' . config('services.pandadoc.api_key'),
'Content-Type' => 'application/json',
])->post("{$this->baseUrl}/documents", [
'name' => "Договор — {$recipient['email']}",
'template' => ['id' => $templateId],
'recipients' => [
[
'email' => $recipient['email'],
'first_name' => $recipient['first_name'],
'last_name' => $recipient['last_name'],
'role' => 'client', // роль из шаблона
],
],
'tokens' => array_map(fn($k, $v) => ['name' => $k, 'value' => $v],
array_keys($tokens), $tokens),
'metadata' => [
'order_id' => $recipient['order_id'] ?? '',
],
]);
return $response->json();
}
}
Токены — это переменные в шаблоне вида [COMPANY_NAME], [CONTRACT_DATE]. При создании документа они подставляются автоматически.
Создание документа из загружаемого PDF
public function createFromPDF(string $pdfPath, array $recipient): array
{
// Шаг 1: загрузить файл
$uploadResponse = Http::withHeaders([
'Authorization' => 'API-Key ' . config('services.pandadoc.api_key'),
])->attach('file', file_get_contents($pdfPath), 'contract.pdf')
->post("{$this->baseUrl}/documents");
$documentId = $uploadResponse->json('id');
// Шаг 2: ждём, пока документ обработается (обычно несколько секунд)
$this->waitForStatus($documentId, 'document.uploaded');
// Шаг 3: добавляем поле подписи
Http::withHeaders([
'Authorization' => 'API-Key ' . config('services.pandadoc.api_key'),
'Content-Type' => 'application/json',
])->patch("{$this->baseUrl}/documents/{$documentId}", [
'recipients' => [[
'email' => $recipient['email'],
'role' => 'Signer',
]],
'fields' => [[
'field_id' => 'sig1',
'type' => 'signature',
'role' => 'Signer',
'page' => 0,
'x' => 100,
'y' => 600,
'width' => 200,
'height' => 50,
]],
]);
return ['id' => $documentId];
}
private function waitForStatus(string $documentId, string $status): void
{
$attempts = 0;
do {
sleep(1);
$doc = Http::withHeaders([
'Authorization' => 'API-Key ' . config('services.pandadoc.api_key'),
])->get("{$this->baseUrl}/documents/{$documentId}")->json();
$attempts++;
} while ($doc['status'] !== $status && $attempts < 15);
}
Отправка на подпись
public function sendDocument(string $documentId, string $message = ''): void
{
Http::withHeaders([
'Authorization' => 'API-Key ' . config('services.pandadoc.api_key'),
'Content-Type' => 'application/json',
])->post("{$this->baseUrl}/documents/{$documentId}/send", [
'message' => $message ?: 'Пожалуйста, ознакомьтесь и подпишите документ.',
'subject' => 'Документ для подписания',
'silent' => false, // true — не отправлять email, только ссылка
]);
}
Embedded signing через session link
public function getSessionLink(string $documentId, string $recipientEmail): string
{
$response = Http::withHeaders([
'Authorization' => 'API-Key ' . config('services.pandadoc.api_key'),
'Content-Type' => 'application/json',
])->post("{$this->baseUrl}/documents/{$documentId}/session", [
'recipient' => $recipientEmail,
'lifetime' => 3600, // секунды
]);
return $response->json('id'); // это session ID, не URL напрямую
// URL для iframe: https://app.pandadoc.com/s/{session_id}
}
Итоговый URL для iframe: https://app.pandadoc.com/s/{session_id}. PandaDoc отправляет postMessage при завершении подписания.
Webhook
// Регистрация в настройках PandaDoc: Settings → API → Webhooks → Add endpoint
public function handlePandaDocWebhook(Request $request): Response
{
// PandaDoc подписывает через HMAC-SHA256, ключ из настроек
$signature = $request->header('x-pandadoc-signature');
$body = $request->getContent();
$expected = hash_hmac('sha256', $body, config('services.pandadoc.webhook_key'));
if (!hash_equals($expected, $signature)) {
abort(403);
}
foreach ($request->json() as $event) {
if ($event['event'] === 'document_state_changed'
&& $event['data']['status'] === 'document.completed') {
$docId = $event['data']['id'];
DownloadPandaDocJob::dispatch($docId);
}
}
return response()->noContent();
}
Обратите внимание: PandaDoc может отправить несколько событий в одном webhook-запросе — итерируем массив.
Скачивание и хранение
public function download(string $documentId, string $savePath): void
{
$response = Http::withHeaders([
'Authorization' => 'API-Key ' . config('services.pandadoc.api_key'),
])->get("{$this->baseUrl}/documents/{$documentId}/download");
file_put_contents($savePath, $response->body());
}
Что отличает PandaDoc
Шаблоны с брендингом (логотип, цвета, стиль), pricing tables прямо в документе (клиент видит сумму и подписывает), approval workflows (документ проходит внутреннее согласование до отправки клиенту), встроенная аналитика просмотра (когда открыли, сколько времени на каждой странице). Это делает PandaDoc подходящим для коммерческих предложений и договоров с ценообразованием, а не только для типовых бланков.
Сроки
Базовая интеграция (шаблон → создание → отправка → webhook → скачивание): 2–3 рабочих дня. С embedded signing, токенами из CRM и approval workflow: 4–5 рабочих дней.







