Documents
Compliance documents (NDAs, IP assignment forms, equipment return acknowledgements, and similar exit paperwork) are stored per-offboarding in the compliance_documents table. There are two ways to create one - upload a file directly, or register a record that points at a file you've already stored elsewhere - and a separate set of endpoints for sending them out for e-signature.
Like the checklist endpoints, these are session-authenticated app routes, not part of /api/v1.
Upload a document
POST /api/upload/compliance/:offboardingId
Content-Type: multipart/form-data
| Field | Type | Description |
|---|---|---|
file | file | PDF, DOC, DOCX, PNG, or JPG - max 10MB |
docType | string | Document category (defaults to Other) |
name | string | Display name (defaults to the uploaded filename) |
The upload is validated against the file's actual magic bytes, not just its extension, and stored in your organization's GCS bucket. Requires the fileUploads plan feature.
Response 201 Created
{
"id": "doc_01HQ3",
"offboarding_id": "e3b0c442-...",
"name": "exit-nda.pdf",
"doc_type": "NDA",
"status": "pending",
"file_url": "compliance/org_123/e3b0c442-.../a1b2c3....pdf",
"created_at": "2024-06-01T10:00:00.000Z"
}file_url is the internal storage key, not a public link - fetch a temporary signed URL via GET /api/upload/files/* when you need to display or download the file.
Register a document by reference
POST /api/employees/:id/compliance
Required permission: compliance_manage - superadmin, admin or hr by default, or a custom role that grants it. Managers can view compliance documents but not add, change or delete them.
Use this instead of the upload endpoint when the file already lives somewhere else (e.g. generated by the document templates endpoint, or hosted externally).
Request body
{
"name": "Exit NDA",
"docType": "NDA",
"fileUrl": "https://storage.example.com/nda.pdf"
}| Field | Required | Description |
|---|---|---|
name | Yes | Document name |
docType | No | Defaults to Other |
fileUrl | No | Must be a public HTTPS URL - private/internal/loopback hosts are rejected |
Response 200 OK - the created compliance_documents row, with status: "pending".
Update a document
PATCH /api/employees/compliance/:docId/status
Required permission: compliance_manage - superadmin, admin or hr by default, or a custom role that grants it.
Request body
| Field | Description |
|---|---|
status | New status - must be one of signed, pending, or completed. Any other value returns 400 { "error": "Invalid status" }. |
signed_by | Who signed it |
signed_at | Signing timestamp |
signature_reference | Where the signed original is, when no file is attached - a ticket number, or where the paper copy is kept |
expires_on | Expiry date, for documents that need periodic renewal |
reason | Why a provider-verified signature is being changed (see below) |
Recording a signature by hand. Moving a document to signed or completed from any other status, when the signature didn't come from an e-signature provider, is a statement that someone signed it, so the record has to say who and on what evidence:
signed_byis required (sent now, or already on the document). Otherwise400with"code": "signer_required".- If the document has no file attached,
signature_reference(at least 3 characters) is required. Otherwise400with"code": "reference_required".
Changing a signature the provider verified. Once an e-signature provider has verified a signature, it is final: changing the document's status, signed_by, signed_at or signature_reference needs an Admin or Super Admin (403 otherwise), plus a reason of 5 to 500 characters that goes into the audit entry (400 with "code": "reason_required" without one).
Response 200 OK - the updated document row.
Returns 409 with "code": "legal_hold" when the document is signed and its offboarding is under legal hold (or was released less than 7 days ago) and the change touches the signature - status, signed_by, signed_at or signature_reference. The signature evidence is frozen while the hold lasts; expires_on can still change.
Delete a document
DELETE /api/employees/compliance/:docId
Required permission: compliance_manage - superadmin, admin or hr by default, or a custom role that grants it.
Deletes the database record and, if a file is attached, removes it from storage too. A reason is required and is written to the audit log.
Request body
{ "reason": "Uploaded against the wrong employee" }| Field | Required | Type | Description |
|---|---|---|---|
reason | ✅ | string | 5 to 500 characters. May be sent as a ?reason= query parameter instead. Missing or out of range returns 400 with "code": "reason_required" |
Response 200 OK
{ "success": true }Returns 404 if the document isn't in your organisation (or department scope), and 409 with "code": "legal_hold" when its offboarding is under legal hold - nothing is deleted; lift the hold first.
Generate from a template
POST /api/document-templates/:id/generate
Renders a saved document template (with merge fields like {{employee_name}} and {{exit_date}} filled in from the offboarding record) into an HTML file and creates a compliance_documents row for it.
Request body
{ "offboardingId": "e3b0c442-..." }Response 201 Created - the created document, plus a short-lived signed_url for immediate viewing.
Deleting a template that has been used
DELETE /api/document-templates/:id returns 409 Conflict when documents have been generated from the template (they keep a reference to it), with a message saying so; nothing is deleted. Rename or edit the template instead. Managing templates needs the compliance_manage permission.
List signed URLs for all compliance documents
GET /api/upload/compliance-zip/:offboardingId
Returns a short-lived signed URL for every compliance document attached to the offboarding, for bulk download/archival.
E-signature
E-signature is a separate feature, routed under /api/esignature, gated behind the esignature plan feature, and works against a document's id from compliance_documents.
Request a signature
POST /api/esignature/request/:documentId
Request body
{ "provider": "docusign" }provider is optional - docusign, adobe_sign, or dropbox_sign. If omitted, PurpletGo uses whichever provider your organization has connected under Integrations, falling back to Dropbox Sign if one is globally configured. Fails with 400 if no provider is available or the document has no file attached, and 409 if a request is already pending.
The request goes to the employee's work email unless you say otherwise. These optional fields control who gets it and how they prove who they are:
| Field | Type | Description |
|---|---|---|
signer_email | string | Send to this address instead of the work email - for example a personal address the departing employee still controls. Must be a valid email address; the work address itself is treated as no override |
access_code | string | 4 to 12 letters or digits the signer must enter before signing. Give it to them outside the email; it is never stored |
signer_phone | string | A mobile number with its country code (E.164, e.g. +44 7700 900123); the provider texts the signer a code they must enter. Stored masked only. Can't be combined with access_code |
acknowledge_mailbox_revoked | boolean | Send to the work email even though the employee's identity provider access has already been revoked (see below) |
A field sent in the wrong form is refused with 400 rather than dropped, so a request never goes out with weaker authentication than you asked for.
If the employee's Google Workspace, Microsoft 365 or Okta access has already been revoked by the offboarding and you haven't given a signer_email, the request is refused with 409 and "code": "signer_mailbox_revoked": the work mailbox may be unreachable, and is in the company's hands, so it is weak evidence of who signed. Send to a personal address, or set acknowledge_mailbox_revoked: true to send to the work email anyway.
Response 200 OK
{ "success": true, "requestId": "abc123", "signingUrl": "https://...", "provider": "docusign" }Check signature status
GET /api/esignature/status/:documentId
Response 200 OK
{ "status": "signed", "requestId": "abc123", "provider": "docusign" }If no signature has ever been requested for this document, the response is just { "status": "not_requested" } with no requestId or provider keys.
Cancel a signature request
POST /api/esignature/cancel/:documentId
Cancels a pending request with the provider and sets the document's esignature_status to cancelled (what GET /status reports). The document's own status is not touched.
Response 200 OK
{ "success": true }