Contracts
Customer contracts — draft, send, sign, amend, and download a signed PDF. End-to-end paperwork for the projects you run through shiply.
shiply contracts are the paper trail for the work you do for customers. A contract is drafted from a project's brief, sent to the customer on the intake portal, signed with a typed name + ESIGN consent, and — optionally — amended later when scope shifts. Once signed, every party can download a tamper-evident PDF with a built-in audit trail.
Contracts attach to projects (shiply's customer-intake workflow). One parent contract per project; amendments stack as additional pages on the same PDF.
Lifecycle
A project moves through four contract-aware states:
brief_ready ──draft──▶ contract_draft ──send──▶ contract_sent
│
(customer signs)
▼
building
│
(amend later if scope shifts)
▼
still buildingbrief_ready— the customer finished the intake wizard; you can draft a contract.contract_draft— the parent contract exists, fields are editable. Customer cannot see it yet.contract_sent— sent to the customer. Field edits are blocked; retract to make further changes.building— customer signed; work is underway. Amendments are legal here (and only here).void— the contract was retracted and not recovered. The project returns tobrief_readyso you can draft a fresh one.
The 8 editable contract fields
Drafts populate from the project's brief, but you can edit any of these on the dashboard's Contract panel before sending:
| Field | Type | Notes |
|---|---|---|
scopeSummary | text | One-paragraph scope of work. Required. |
feeCents | integer | Total fee in cents (e.g. 250000 for $2,500.00). |
currency | string | ISO 4217 code (USD, AUD, EUR, GBP, etc.). |
startDate | date | ISO YYYY-MM-DD. When work begins. |
targetCompletionDate | date | ISO YYYY-MM-DD. The handoff date. |
revisionCount | integer | Included revision rounds. 0 is fine. |
revisionOverageCents | integer | Per-revision fee for overages (in cents). |
jurisdiction | string | Governing law (e.g. Victoria, Australia). |
The contract template renders these into a 9-section Markdown body:
- Parties — developer and customer details.
- Scope of work —
scopeSummaryverbatim. - Fee and payment schedule — fee in
currency, deposit/balance terms. - Timeline —
startDate→targetCompletionDate. - Revisions — count included + overage fee.
- Intellectual property and ownership — assignment on full payment.
- Scope changes and amendments — points to the amendment workflow.
- Handoff and delivery — what the customer receives.
- Jurisdiction and signatures — governing law + typed-signature block.
The rendered Markdown is hashed (SHA-256 → contentHash on the contract
row) at send time. The signed PDF embeds the hash so any later edit
to the Markdown is detectable.
Sign UX (ESIGN-style)
The customer reaches the contract via the intake portal (or a deep link from the send email). They:
- Read the rendered Markdown inline.
- Type their full legal name in the signature field.
- Tick the ESIGN consent box ("I agree to be legally bound…").
- Sign.
The server records:
signedAt(UTC timestamp),signedName(the typed name),signedIpAddress(request IP),signedUserAgent,contentHash(the hash of the Markdown they actually agreed to).
These five fields are the audit trail. They print on the certificate page of the PDF and they're what makes the signature defensible in an ESIGN or eIDAS jurisdiction.
Amendments
Once a parent contract is signed, the project moves to building and
you can issue amendments — short add-ons that change scope, fee, or
the target date without redrafting the whole agreement.
Each amendment captures three deltas (all optional except scopeDelta):
scopeDelta— text describing the change (required).feeDeltaCents— integer; positive for adds, negative for discounts.targetCompletionDate— new ISO date if the timeline shifts.
Amendments follow the same draft → send → sign loop as the parent, but
they don't render their own contract body — they sign a structured JSON
blob that's content-hashed the same way. The signed PDF stacks one
[amendment page + certificate page] pair per signed amendment after
the parent.
You can have multiple signed amendments. There can only ever be one live parent contract per project — retract the existing one before drafting a new one.
PDF + audit trail
shiply contract pdf <contract-id> -o contract.pdfThe PDF is rendered server-side via the contract-pdf pipeline (B6). It contains:
- The full Markdown body of the parent contract, paginated.
- A certificate page with the five audit fields + the
contentHash. - One
[page + certificate]pair per signed amendment. - Tamper-evident: the certificate page references the same hash the customer agreed to. Re-rendering after any field change produces a different hash, which is obvious on the page.
The endpoint refuses to render before the parent is signed (409
contract_not_signed). Cache headers are private, no-store — never
cache a contract PDF on a shared cache.
CLI
# Lifecycle on a project
shiply contract list <project-id> # parent + amendments + status
shiply contract draft <project-id> # create a draft you can edit
shiply contract show <contract-id> # full row as JSON
# Sending + signing
shiply contract send <contract-id> # email the customer
shiply contract pdf <contract-id> -o file.pdf # download signed PDF
# After signing
shiply contract amend <contract-id> \
--scope "Add a second landing page" \
--fee-delta 50000 # +$500.00
shiply contract amend <contract-id> \
--scope "Push handoff back two weeks" \
--target-date 2026-08-15
# Undo
shiply contract retract <contract-id> # void (project → brief_ready)
shiply contract retract <contract-id> --keep-as-draft # edit + re-sendshiply contract amend drafts the amendment AND immediately sends it —
amendments are a quick-fix path. Edit the dashboard panel if you want to
hold an amendment as a draft.
REST API (Authorization: Bearer shp_…)
| Method & path | Purpose |
|---|---|
GET /api/v1/projects/{id}/contracts | parent + amendments for a project |
POST /api/v1/projects/{id}/contract | create a draft (project must be brief_ready) |
GET /api/v1/contracts/{id} | one contract — readable by dev OR by the portal cookie |
PATCH /api/v1/contracts/{id} | edit the 8 draft fields (draft only, dev only) |
POST /api/v1/contracts/{id}/send | send a draft (parent or amendment) |
POST /api/v1/contracts/{id}/sign | customer-signed via the portal — typed name + ESIGN |
POST /api/v1/contracts/{id}/amend | create an amendment draft against a signed parent |
POST /api/v1/contracts/{id}/retract | retract; pass {recoverAsDraft: true} to keep as draft |
POST /api/v1/contracts/{id}/remind | dev-triggered nudge — emails the customer again |
POST /api/v1/contracts/{id}/resend | re-send the same email |
GET /api/v1/contracts/{id}/view | rendered Markdown (used by the portal) |
GET /api/v1/contracts/{id}/pdf | signed-contract PDF (409 until signed) |
The dev surface authenticates with an API key; the customer surface
authenticates with the shiply-portal-token cookie issued by
/intake/{token}. The PATCH and POST routes are dev-only — the customer
can read and sign but not edit.
MCP — 5 contract tools
Every shiply MCP server exposes these tools (shipped in B13):
contract_status—{projectId}→ parent + amendments + state. Same payload shape asshiply contract list.contract_draft—{projectId}→ drafts a parent contract.contract_send—{contractId}→ sends a draft.contract_amend—{parentContractId, scopeDelta, feeDeltaCents?, targetCompletionDate?}→ drafts + sends an amendment.contract_pdf—{contractId}→ returns the signed PDF as a base64 string (withmimeType: application/pdf). 409 if not yet signed.
This is the same surface as the CLI and the dashboard — an agent operating a customer project end-to-end can run the contract flow with just these five tools.
Errors
| Status | error.code | Meaning |
|---|---|---|
| 401 | unauthorized | missing or invalid API key / portal cookie |
| 403 | forbidden | not your project / contract |
| 404 | not_found | bad <id> |
| 409 | conflict | wrong state — see message (e.g. contract_not_signed) |
| 409 | state_violation | project state doesn't allow this op (per the state machine) |
| 400 | invalid_request | bad body shape, missing required field, bad date |
| 422 | template_incomplete | sent with one of the 8 fields still empty |
The state machine is strict — POST /send on a project that's already in
contract_sent returns state_violation rather than silently re-sending.
Use POST /resend to fire another email for the same contract.