Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ assinafy-php-sdk/
│ │ ├── SignerResource.php
│ │ ├── AssignmentResource.php
│ │ ├── TemplateResource.php
│ │ ├── TagResource.php
│ │ ├── FieldResource.php
│ │ ├── WebhookResource.php
│ │ ├── AuthResource.php
│ │ └── SignerSessionResource.php
│ │ ├── SignerSessionResource.php
│ │ └── SignerDocumentResource.php
│ └── Support/ # Helper classes
│ └── WebhookVerifier.php
├── tests/ # PHPUnit test suites
Expand Down Expand Up @@ -69,13 +72,16 @@ $client->documents()->upload(...);

Resource classes encapsulate domain logic for each API resource:

- **DocumentResource**: Document operations, including artifacts, public endpoints, and template-driven creation
- **DocumentResource**: Document operations, including artifacts, public endpoints, document tags, and template-driven creation
- **SignerResource**: Workspace signer CRUD
- **AssignmentResource**: Signature requests (virtual + collect), cost estimation, resend, expiration reset
- **AssignmentResource**: Signature requests (virtual + collect), cost estimation, resend, expiration reset, WhatsApp notification history
- **TemplateResource**: Template listing and retrieval
- **WebhookResource**: Webhook subscription upsert / read / delete
- **TagResource**: Workspace tag CRUD
- **FieldResource**: Field-definition CRUD, value validation, and the global type catalog
- **WebhookResource**: Webhook subscription upsert / read / inactivate, plus dispatch history, retry, and event-type discovery
- **AuthResource**: Login, social login, API-key lifecycle, password reset / change
- **SignerSessionResource**: Signer-facing endpoints authenticated with a `signer-access-code`
- **SignerSessionResource**: Signer-facing session endpoints authenticated with a `signer-access-code` (identity, signature image, sign/decline)
- **SignerDocumentResource**: Signer-facing document list / sign-multiple / decline-multiple / download, authenticated with a `signer-access-code`

**Pattern**: Each resource extends `AbstractResource` and follows the same structure.

Expand Down Expand Up @@ -172,7 +178,7 @@ class CustomHttpClient implements HttpClientInterface { }
abstract class AbstractResource
{
protected function extractData(array $response): array { }
protected function normalizeId(array $data): array { }
protected function accountPath(string $suffix = ''): string { }
}
```

Expand Down
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,57 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.4.0] - 2026-05-27

Complete coverage pass against `https://api.assinafy.com.br/v1/docs`. A full re-read of
the live documentation surfaced several whole resource families the SDK had never exposed;
each new endpoint below was verified end-to-end against the production API before release.

### Added

- **`TagResource`** (`$client->tags()`) — workspace tag management:
`GET/POST /accounts/{id}/tags`, `PUT/DELETE /accounts/{id}/tags/{tag_id}` (with `force`
detach-and-delete).
- **`FieldResource`** (`$client->fields()`) — field-definition management and validation:
`POST/GET /accounts/{id}/fields`, `GET/PUT/DELETE /accounts/{id}/fields/{field_id}`,
`POST …/fields/{id}/validate`, `POST …/fields/validate-multiple` (both usable as an
authenticated user or, with a `signer-access-code`, as a signer), and the global
`GET /field-types` catalog.
- **`SignerDocumentResource`** (`$client->signerDocuments()`) — signer-facing document
endpoints authenticated by `signer-access-code`: `GET /signers/{id}/document`,
`GET /signers/{id}/documents`, `PUT /signers/documents/sign-multiple`,
`PUT /signers/documents/decline-multiple`, and
`GET /signers/{id}/documents/{id}/download/{artifact_name}`.
- **`DocumentResource` document tags** — `listTags()`, `appendTags()`, `replaceTags()`,
`detachTag()` covering `GET/POST/PUT /accounts/{id}/documents/{id}/tags` and
`DELETE …/tags/{tag_id}`.
- **`AssignmentResource::whatsappNotifications()`** — `GET /documents/{id}/assignments/{id}/whatsapp-notifications`.
- **`AssignmentResource` sequential signing** — signer entries now pass through the
documented `step` field; added `NOTIFICATION_EMAIL` / `NOTIFICATION_WHATSAPP` constants.
- **`SignerSessionResource`** signer-facing signing actions — `currentDocument()`
(`GET /sign`), `sign()` (`POST /documents/{id}/assignments/{id}`), and `decline()`
(`PUT /documents/{id}/assignments/{id}/reject`).
- **`WebhookResource`** dispatch + discovery endpoints — `eventTypes()`
(`GET /webhooks/event-types`), `dispatches()` (`GET /accounts/{id}/webhooks`, paginated
with `event`/`delivered`/`from`/`to` filters), and `retryDispatch()`
(`POST /accounts/{id}/webhooks/{dispatch_id}/retry`). Added constants for all 15
subscribable event types.
- **Query-string parameter** on `HttpClientInterface::delete()` — supports the tag
`?force=true` flag. Backward-compatible: the new `$query` arg is the third positional
and defaults to `[]`.
- **4 new live integration tests** covering the tag, field, document-tag, and webhook
discovery endpoints (all credit-free).

### Changed

- **`WebhookResource::deactivate()`** now calls the dedicated `PUT /accounts/{id}/webhooks/inactivate`
endpoint (verified live) instead of re-`PUT`ting the subscription with `is_active: false`.
The server preserves the URL / email / events, so `activate()` still restores them.
- **`DocumentResource::assertArtifact()`** promoted to `public static` so
`SignerDocumentResource::download()` validates artifact names through the same list (DRY).

[1.4.0]: https://github.com/assinafy/php-sdk/releases/tag/v1.4.0

## [1.3.0] - 2026-05-12

Second pass against `https://api.assinafy.com.br/v1/docs` plus a full live verification
Expand Down
88 changes: 79 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Modern, framework-agnostic PHP SDK for the [Assinafy](https://assinafy.com.br) digital signature API (`https://api.assinafy.com.br/v1`). Built with PSR standards and SOLID principles.

The SDK covers **every documented endpoint** in `https://api.assinafy.com.br/v1/docs` plus the webhook subscription endpoints, and is verified against the live API by an integration test suite.
The SDK covers **100% of the documented endpoints** in `https://api.assinafy.com.br/v1/docs` — documents and document tags, signers, signer sessions and signer documents, assignments, templates, workspace tags, field definitions, authentication, and the full webhooks surface (subscription, dispatch history, retry, event types). Every endpoint is verified against the live API by an integration test suite.

## Features

Expand Down Expand Up @@ -139,6 +139,10 @@ Every endpoint exposed by the documented API is reachable through the SDK. Resou
| `sendToken($documentId, $recipient, $channel)` | `PUT /public/documents/{id}/send-token` |
| `createFromTemplate($templateId, $signers, $options)` | `POST /accounts/{id}/templates/{id}/documents` |
| `estimateCostFromTemplate($templateId, $signers)` | `POST /accounts/{id}/templates/{id}/documents/estimate-cost` |
| `listTags($documentId)` | `GET /accounts/{id}/documents/{id}/tags` |
| `appendTags($documentId, $tagNames)` | `POST /accounts/{id}/documents/{id}/tags` |
| `replaceTags($documentId, $tagNames)` | `PUT /accounts/{id}/documents/{id}/tags` |
| `detachTag($documentId, $tagId)` | `DELETE /accounts/{id}/documents/{id}/tags/{tag_id}` |
| `waitUntilReady($documentId, $maxWait, $pollInterval)` | polls `GET /documents/{id}` |
| `isFullySigned($documentId)` | derived from `GET /documents/{id}` |
| `getSigningProgress($documentId)` | derived from `GET /documents/{id}` |
Expand Down Expand Up @@ -204,16 +208,19 @@ $client->signers()->delete($signer['id']);
| `resend($documentId, $assignmentId, $signerId)` | `PUT /documents/{id}/assignments/{id}/signers/{id}/resend` |
| `estimateResendCost($documentId, $assignmentId, $signerId)` | `POST /documents/{id}/assignments/{id}/signers/{id}/estimate-resend-cost` |
| `resetExpiration($documentId, $assignmentId, $expiresAt)` | `PUT /documents/{id}/assignments/{id}/reset-expiration` |
| `whatsappNotifications($documentId, $assignmentId)` | `GET /documents/{id}/assignments/{id}/whatsapp-notifications` |

```php
use Assinafy\SDK\Resources\AssignmentResource;

// Virtual assignment (no input fields). Signers may be ID strings or full objects.
// Add `step` to a signer for sequential signing (signers in the same step sign in
// parallel; the next step is notified only once the previous step has all signed).
$assignment = $client->assignments()->create(
documentId: $documentId,
signers: [
$signerId1,
['id' => $signerId2, 'verification_method' => AssignmentResource::VERIFICATION_WHATSAPP],
['id' => $signerId1, 'step' => 1],
['id' => $signerId2, 'verification_method' => AssignmentResource::VERIFICATION_WHATSAPP, 'step' => 2],
],
method: AssignmentResource::METHOD_VIRTUAL,
options: [
Expand Down Expand Up @@ -247,18 +254,60 @@ foreach ($template['roles'] as $role) {
}
```

### Tags — `$client->tags()`

Workspace-scoped labels that can be attached to documents and templates. Tag names are unique per workspace (case-insensitive).

| Method | Endpoint |
| --- | --- |
| `list($search)` | `GET /accounts/{id}/tags` |
| `create($name, $color)` | `POST /accounts/{id}/tags` |
| `update($tagId, $data)` | `PUT /accounts/{id}/tags/{id}` |
| `delete($tagId, $force)` | `DELETE /accounts/{id}/tags/{id}` |

```php
$tag = $client->tags()->create('Contracts', 'ff8800');
$client->tags()->update($tag['id'], ['name' => 'Sales Contracts']);
$client->tags()->delete($tag['id'], force: true); // detach from everything, then delete
```

### Field definitions — `$client->fields()`

Reusable inputs (text, CPF, e-mail, date, …) that can be placed on a document for `collect` assignments.

| Method | Endpoint |
| --- | --- |
| `create($type, $name, $options)` | `POST /accounts/{id}/fields` |
| `list($includeInactive, $includeStandard)` | `GET /accounts/{id}/fields` |
| `get($fieldId)` | `GET /accounts/{id}/fields/{id}` |
| `update($fieldId, $data)` | `PUT /accounts/{id}/fields/{id}` |
| `delete($fieldId)` | `DELETE /accounts/{id}/fields/{id}` |
| `validate($fieldId, $value, $signerAccessCode)` | `POST /accounts/{id}/fields/{id}/validate` |
| `validateMultiple($values, $signerAccessCode)` | `POST /accounts/{id}/fields/validate-multiple` |
| `types()` | `GET /field-types` |

```php
$field = $client->fields()->create('cpf', 'Taxpayer ID');
$check = $client->fields()->validate($field['id'], '400.676.228-36'); // ['success' => true, …]
$types = $client->fields()->types(); // [['type' => 'text', 'name' => 'Text'], …]
```

### Webhooks — `$client->webhooks()`

| Method | Endpoint |
| --- | --- |
| `register($url, $email, $events, $isActive)` | `PUT /accounts/{id}/webhooks/subscriptions` |
| `get()` | `GET /accounts/{id}/webhooks/subscriptions` |
| `deactivate()` | `PUT …/subscriptions` with `is_active: false` |
| `activate()` | `PUT …/subscriptions` with `is_active: true` |
| `deactivate()` | `PUT /accounts/{id}/webhooks/inactivate` |
| `activate()` | re-sends the stored config via `PUT …/subscriptions` with `is_active: true` |
| `eventTypes()` | `GET /webhooks/event-types` |
| `dispatches($filters)` | `GET /accounts/{id}/webhooks` |
| `retryDispatch($dispatchId)` | `POST /accounts/{id}/webhooks/{dispatch_id}/retry` |

> The v1 API has no `DELETE` route for webhook subscriptions (it returns 404). The
> way to stop receiving events is `deactivate()` — the configuration is preserved
> so you can `activate()` again later. `is_active` is required in the request body.
> The v1 API has no `DELETE` route for webhook subscriptions. The way to stop
> receiving events is `deactivate()` (the dedicated `inactivate` endpoint) — the
> configuration is preserved so you can `activate()` again later. `register()`
> requires `url`, `email`, `events`, and `is_active`.

```php
use Assinafy\SDK\Resources\WebhookResource;
Expand All @@ -268,6 +317,12 @@ $client->webhooks()->register(
email: 'admin@your-domain.com',
events: WebhookResource::DEFAULT_EVENTS,
);

// Inspect and replay delivery history
$history = $client->webhooks()->dispatches(['delivered' => 'false']);
foreach ($history['data'] as $dispatch) {
$client->webhooks()->retryDispatch($dispatch['id']);
}
```

### Authentication — `$client->auth()`
Expand Down Expand Up @@ -302,6 +357,21 @@ Endpoints authenticated with a signer's `signer-access-code` (not the workspace
| `confirmData($documentId, $accessCode, $data)` | `PUT /documents/{id}/signers/confirm-data` |
| `uploadSignature($accessCode, $type, $bytes, $mime)` | `POST /signature` |
| `downloadSignature($accessCode, $type)` | `GET /signature/{type}` |
| `currentDocument($accessCode)` | `GET /sign` |
| `sign($documentId, $assignmentId, $accessCode, $fields)` | `POST /documents/{id}/assignments/{id}` |
| `decline($documentId, $assignmentId, $accessCode, $reason)` | `PUT /documents/{id}/assignments/{id}/reject` |

### Signer documents (signer-facing) — `$client->signerDocuments()`

Document list/sign/decline/download for a signer, authenticated by `signer-access-code`.

| Method | Endpoint |
| --- | --- |
| `current($signerId, $accessCode)` | `GET /signers/{id}/document` |
| `list($signerId, $accessCode, $filters)` | `GET /signers/{id}/documents` |
| `signMultiple($accessCode, $documentIds)` | `PUT /signers/documents/sign-multiple` |
| `declineMultiple($accessCode, $documentIds, $reason)` | `PUT /signers/documents/decline-multiple` |
| `download($signerId, $documentId, $accessCode, $artifact)` | `GET /signers/{id}/documents/{id}/download/{artifact_name}` |

## Webhook signature verification

Expand Down Expand Up @@ -371,7 +441,7 @@ vendor/bin/phpstan analyse
vendor/bin/phpcs
```

**Current status**: PSR-12 compliant · PHPStan level 8 (zero errors) · 73 unit tests + 6 live integration tests · PHP 7.4 – 8.4 compatible.
**Current status**: PSR-12 compliant · PHPStan level 8 (zero errors) · 116 unit tests + 17 live integration tests · PHP 7.4 – 8.4 compatible.

## License

Expand Down
69 changes: 67 additions & 2 deletions docs/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,17 @@ $client->webhooks()->register(
);

$current = $client->webhooks()->get();
$client->webhooks()->delete();

// Stop / resume delivery (the v1 API has no DELETE for subscriptions)
$client->webhooks()->deactivate(); // PUT /accounts/{id}/webhooks/inactivate
$client->webhooks()->activate(); // re-sends stored config with is_active: true

// Discover subscribable events and inspect / replay delivery history
$types = $client->webhooks()->eventTypes(); // GET /webhooks/event-types
$history = $client->webhooks()->dispatches(['delivered' => 'false']); // GET /accounts/{id}/webhooks
foreach ($history['data'] as $dispatch) {
$client->webhooks()->retryDispatch($dispatch['id']); // POST …/webhooks/{id}/retry
}
```

## 12. Webhook receiver
Expand Down Expand Up @@ -261,9 +271,64 @@ $session->uploadSignature(

// Download the stored signature
$png = $session->downloadSignature($accessCode, 'signature');

// View, sign (collect method) and decline as the signer
$current = $session->currentDocument($accessCode); // GET /sign
$session->sign($documentId, $assignmentId, $accessCode, [ // POST /documents/{id}/assignments/{id}
['itemId' => 'i1', 'fieldId' => 'f1', 'pageId' => 'p1', 'value' => 'Signed by John'],
]);
$session->decline($documentId, $assignmentId, $accessCode, 'I do not agree with clause 2.');
```

## 15. Signer documents (signer-facing list / bulk sign / download)

```php
$docs = $client->signerDocuments();

$current = $docs->current($signerId, $accessCode); // GET /signers/{id}/document
$list = $docs->list($signerId, $accessCode, ['status' => 'pending_signature']);

$docs->signMultiple($accessCode, ['doc1', 'doc2']); // PUT /signers/documents/sign-multiple
$docs->declineMultiple($accessCode, ['doc3'], 'Unfavorable terms.');

$pdf = $docs->download($signerId, 'doc1', $accessCode, \Assinafy\SDK\Resources\DocumentResource::ARTIFACT_ORIGINAL);
```

## 16. Workspace tags

```php
$tag = $client->tags()->create('Contracts', 'ff8800'); // POST /accounts/{id}/tags
$client->tags()->list('contract'); // GET /accounts/{id}/tags?search=contract
$client->tags()->update($tag['id'], ['name' => 'Sales Contracts']);
$client->tags()->delete($tag['id'], force: true); // detach from everything, then delete
```

## 17. Document tags

```php
$client->documents()->appendTags($documentId, ['Urgent', 'Q1-2026']); // POST …/documents/{id}/tags
$client->documents()->listTags($documentId); // GET …/documents/{id}/tags
$client->documents()->replaceTags($documentId, ['Signed']); // PUT …/documents/{id}/tags
$client->documents()->detachTag($documentId, $tagId); // DELETE …/tags/{tag_id}
```

## 18. Field definitions and validation

```php
$field = $client->fields()->create('cpf', 'Taxpayer ID'); // POST /accounts/{id}/fields
$client->fields()->list(includeStandard: true); // GET /accounts/{id}/fields
$client->fields()->update($field['id'], ['name' => 'CPF']);

// Validate a value — as an authenticated user, or as a signer via the access code
$result = $client->fields()->validate($field['id'], '400.676.228-36'); // ['success' => true, …]
$client->fields()->validateMultiple([
['field_id' => $field['id'], 'value' => '400.676.228-36'],
], $signerAccessCode);

$types = $client->fields()->types(); // GET /field-types
```

## 15. Error handling
## 19. Error handling

```php
use Assinafy\SDK\Exceptions\ApiException;
Expand Down
Loading
Loading