Contracts
Contracts are block-composed legal documents (booking confirmation, terms, cancellation policy, etc.) that the customer signs either in-browser via the public signing page or by uploading a wet-signed PDF back to you. Once signed by the customer and countersigned by the admin, the stored PDF gets a SHA-256 hash committed so you can later verify it hasn’t been tampered with.
Seeded contract blocks are examples only. They are written as templates in plain German and English but have not been reviewed for any specific jurisdiction. Before you ever send a contract to a customer, have a lawyer in your jurisdiction review the blocks you intend to use. The admin UI surfaces this reminder near the Send action; do not dismiss it lightly.
Prerequisites
- Feature flag:
contractsenabled in Settings → Features. - Business profile: complete (
Settings → CRM → Business profile) — the issuer block appears on every contract. - Block library populated: open CRM → Contracts → Block Library at least once after enabling the feature; the self-heal seed inserts the example block set on first access.
Block library
A contract is composed from named blocks (e.g. “Booking Fee Clause”, “Cancellation Policy”, “Copyright Transfer”). Each block has:
- A section label (
preamble,body,payment,cancellation,delivery, etc.) — used to group blocks visually in the contract editor and on the rendered PDF. - A body in plain text or basic markdown — rendered into the PDF section.
- An optional language filter — DE blocks won’t appear in an EN contract picker and vice versa.
You can edit the seeded blocks, add your own, or disable blocks you don’t use. The library lives at CRM → Contracts → Block Library.
When a contract includes a block, the body text is snapshotted into contract_block_inclusions at composition time. Editing the source block later does NOT alter contracts that already included it — the customer who signed yesterday’s contract sees yesterday’s wording on download, not today’s edits.
Composing a contract
From CRM → Contracts, click New contract. The editor surfaces:
- Customer + business + language + currency — same shape as the quote editor.
- Title — the contract title shown to the customer (“Booking Confirmation — Smith Wedding”).
- Block picker — multi-select from the library. Drag to reorder within each section.
- Signing-mode toggle — in-browser canvas signature, wet-signed PDF upload, or both.
- Linked event — optional. If set, the lineage card on the event shows the contract; signing the contract can optionally trigger a status update on the event.
Sending a contract
Contracts start from a template; see Contract Templates & PDF Design for templates, attachments, the PDF theme, and what is frozen at send. In builds with the pre-send review, Send to customer first shows what will go out — signers, attachments, price, template version and any problems — and sends from there.
Send transitions the contract from draft → sent:
- Re-renders the PDF from the current block snapshots, business profile, and customer data.
- Persists the PDF to storage.
- Mints a public action token (
contract_action_tokens) for the signing flow. - Queues the
contract_sentemail to the customer (primary email — contracts are decision-maker correspondence, not billing).
Customer signing flow
Signers, declarations, reminders, expiry, collecting missing details, the legal notice, what is kept and the itemised integrity report are described in Signing contracts. Those are on the beta channel only; stable (v3.134.x) signs contracts with the single-signer flow on this page.
The customer follows the link in the email to /contracts/sign/<token>. In builds containing PicPeak PR #1465 , the page first shows only your company and a masked hint of the customer’s email address, and asks for a 6-digit code that PicPeak emails to the address on file. The code is valid for 15 minutes and allows five attempts, and sending is throttled to one code per minute and five per hour per link. Confirming it opens the contract in that browser for 30 minutes. Customers with a portal account can read and sign from the portal instead, without a code.
After verification they see:
- The contract content, fully rendered (issuer block, recipient block, all included blocks in order)
- The PDF download link
- The signing affordance based on the contract’s signing-mode:
- In-browser canvas: draw signature with mouse / finger on a
signature_padcanvas, type name, click Sign. - PDF upload: download the contract PDF, sign it physically, scan / photograph it, upload the signed PDF back.
- Both: pick either path.
- In-browser canvas: draw signature with mouse / finger on a
On submit:
- The signature image (canvas) or the uploaded signed PDF is persisted alongside the contract. An uploaded file must really be a PDF; anything else is refused (builds containing PicPeak PR #1471 ). In builds containing PicPeak PR #1809 a PDF uploaded by the customer is also inspected before it is accepted: at most 10 MB, not encrypted, and without scripts, actions, forms or embedded files. A PDF that defines the same object twice in a way viewers and the check could read differently is refused (
PDF_AMBIGUOUS_OBJECTS); exporting it again from the scanner or PDF app fixes that. The file is stored exactly as uploaded. Once a contract is fully signed, a customer upload can no longer replace its PDF (CONTRACT_ALREADY_SIGNED). - The contract transitions to
signed. - The admin gets the
contract_signed_admin_notificationemail.
Countersigning
Once the customer has signed, the admin sees a Countersign action on the detail page. Countersigning:
- Adds the admin’s signature (canvas drawing, same as the customer flow) to the stored document.
- Re-renders the final PDF with both signatures.
- Computes a SHA-256 hash of the final PDF bytes and stores it in
contracts.integrity_hash. - Transitions the contract to
fully_signed. - Queues the
contract_fully_signedemail to the customer with the final PDF attached.
The fully-signed email only goes out with a PDF that carries both signatures. If the stamped PDF could not be recorded — for example because a re-stamp or an upload changed it at the same moment — no email is sent yet: the contract page shows a banner, and Re-send signed PDF sends the emails once the PDF is consistent. Re-stamping or re-sending is refused when a signature image cannot be stamped onto the PDF. (Builds containing PicPeak PR #1469 .)
After countersigning, the contract is read-only for both parties.
Integrity check
The current report checks far more than the PDF — see Checking a signed contract.
Once a contract has an integrity_hash, the admin detail page shows a Verify integrity action. The current button re-reads the stored PDF, recomputes the SHA-256, and compares it to the stored hash:
- ✅ Hash matches → “Integrity verified” toast
- ❌ Hash mismatch → “PDF has been modified since signing” error — investigate immediately (filesystem corruption, manual file replacement, backup-restore inconsistency)
A planned hardening pass adds an upload-and-verify variant where you can drop a PDF (e.g. the customer emailed you “their copy” of the signed contract) and verify it against the stored hash. Useful for catching after-the-fact tampering attempts. Not yet shipped — see project backlog.
Storno / cancellation
Contracts don’t have a Storno path. To cancel a fully_signed contract:
- Issue a written cancellation outside PicPeak (the legal mechanism varies — your lawyer will know the right shape).
- Mark the contract
cancelledon the admin detail page — this is a status change, not a re-signing event. The original signed PDF and integrity hash stay intact for audit.
If you also need to refund money already invoiced under this contract, that’s an invoice Storno on the related invoice(s).
What erasing a customer keeps
Available on the beta channel from v3.135.0-beta.0 (PicPeak/picpeak PR 1545), not on stable. Earlier builds and stable anonymise the customer account but leave their contracts untouched.
GDPR-erasing a customer anonymises their account. What happens to their contracts depends on whether anyone has signed them yet.
A contract with at least one signature is kept. This covers a signature by the customer, by any co-signer or by you as the issuer, and any contract in signed_by_customer, signed_by_admin or fully_signed status. Such a contract is the record of a concluded, or partly concluded, agreement, and commercial and tax law generally requires you to keep it, and the evidence of how it was signed, for the statutory retention period. Erasure therefore only ends access to it.
A contract that nobody has signed is redacted. Nothing was agreed, so there is nothing to keep. This applies whatever its status: a draft or sent contract is also set to cancelled, while a contract the customer already declined or that was already cancelled keeps its status and only loses the personal data.
| Data | Contract with a signature | Contract with no signature |
|---|---|---|
| Contract row and status | Kept, status unchanged | Kept. draft / sent is set to cancelled; declined and cancelled stay unchanged |
| Signing links, signing sessions and action tokens | Revoked | Revoked |
| Signer records: encrypted name, email address, IP address, user agent and decline reason, and the email lookup hash | Kept, for every signer, including co-signers who never signed | Cleared, for every signer |
| Customer name and address in the frozen contract text | Kept | Blanked. The stored content hash is not recomputed; the contract records that its text was redacted on erasure, so a later integrity check does not read the change as tampering |
| Stored PDFs, signature images, signing certificate | Kept | Not deleted. An unsigned contract has no signature images, but the PDF generated when it was sent stays in storage |
| Signing event log | Kept | Kept, with a new revoked event. Labels already in the log, such as the name of a signer who opened the contract, stay |
| Documents the customer uploaded and linked to the contract | Kept and unshared; the file name is replaced with erased.pdf | Same |
Other points:
- The signing event log cannot be edited. Each event’s hash covers the event before it, so changing or removing a label would break the chain for every later event. Names already recorded there are retained, including on a redacted contract.
- A signature decides, not the status. A contract that was signed and later ended up
cancelledis still kept whole; adeclinedorcancelledcontract nobody signed is redacted, including the decline reason. - Only the
customer_nameandcustomer_addressplaceholders are redacted. Text you typed into the contract yourself, such as the title, an event name or a clause mentioning the customer, is not touched. - Portal documents not linked to a contract are deleted with the account, and their files are removed from storage.
- The customer’s activity log entry for the erasure lists which contracts were cancelled, which were redacted without a status change, and which were kept.
PicPeak does not delete a kept contract when its retention period ends.
Per-customer feature override
customer_accounts.feature_contracts toggle on the customer detail page hides the contract surface for that customer when off. Useful for casual / wedding clients where a contract isn’t part of the workflow.