Appearance
Contracts and e-signatures
- Where exactly is a contract stored, and is the Google Drive copy the source of truth?
- What is the signing-link token and how guessable is it?
- Can the rich-text body inject script onto the public signing page?
- What is recorded as the e-signature, and how is the IP protected from spoofing?
- What stops a contract from being signed twice or edited after signing?
- How does ShootCal calculate contract line items and totals?
- Is the editor loaded from a CDN, and what happens to old plain-text contracts?
- Does ShootCal email the signing link to the client automatically?
- Is this a legally binding contract / legal advice?
ShootCal has a built-in way to write a photography contract, send it to a client, and have them sign it online. No DocuSign, no PDFs, no printing.
It works in three steps:
- You set up one master template once. It is a normal rich-text document (headings, bold, bullet lists) written in a built-in editor. You drop in placeholders like {client}, {date}, {package}, and {price}, and ShootCal fills those in for each contract. If you never edit the template, ShootCal starts you with a sensible general-purpose one.
- You generate a contract for a specific job. You fill in the client, date, and location, then list each client-visible service with a quantity and unit price. Picking one of your saved products or services adds it as a line item. Additional hours are simply another visible line item. ShootCal calculates the exact total and keeps the retainer/deposit separate.
- You choose Preview, Save & Send. ShootCal opens the exact contract the client will see. Nothing is sent until you review that preview and choose Save & Send; you can instead save the same reviewed version as a draft. The client receives the private signing link, reads the agreement, types their full name, ticks the agree box, and taps Sign. The signed contract is then locked and you receive a notification.
Each contract shows a status the whole way through: Draft, Sent, or Signed. One important note that ShootCal shows on every contract: ShootCal is not a law firm and the templates are a convenience, not legal advice. The terms are yours to review and adapt.
Start a contract from a client
Open a client in Clients & Booking, then use the Paperwork card’s Send a contract action. ShootCal opens Payments › Contracts with that client’s name and email already filled, merges your current contract template, and applies your saved photographer countersignature when autosign is enabled. Nothing is saved or emailed until you review the editor and deliberately choose a contract action.
The native iPhone, iPad, Mac, and Android client views can use the same web editor. They redeem a short-lived, single-use browser handoff, then open the signed-in Contracts workspace with the client prefilled. Client details ride only in the URL fragment, which the web app removes before parsing so names and email addresses never enter server access logs. If an interrupted contract create is already retained, that recoverable draft takes precedence over a new prefill and the editor identifies the recovery state instead of silently discarding earlier work.
Some calendar and native entry points can prefill visible client, date, or location fields before the editor opens. That convenience is not a durable calendar-booking link for new contracts in this release. If the native app and browser are signed in to different ShootCal accounts, ShootCal asks before switching and leaves the browser account untouched when you choose to stay.
The end-to-end flow (who does what, in order)
The contract flow spans the ShootCal backend and the web app UI. The flow:
- Template (one per user). The backend returns your saved master template body, or a built-in general-purpose template if you have never saved one. Saving upserts it. The body is rich HTML authored in a self-hosted rich-text editor.
- Preview an exact snapshot. The editor has one main action: Preview, Save & Send. ShootCal freezes the current fields and line items, builds a server-rendered preview from that snapshot, and keeps using that exact reviewed snapshot even if the editor changes underneath it.
- Save or send from the preview. Save draft stores the reviewed snapshot without emailing it. Save & Send first stores that same snapshot, then flips a
drafttosent, stamps the send time, and asks the photographer’s connected Gmail account to email the private signing link. A resend requires a separate confirmation. ShootCal retains interrupted or uncertain attempts and checks the reviewed contract revision again before a safe retry; an unverifiable or changed contract must be reloaded and previewed again. - Client opens the link. The public link (no login) renders a self-contained signing page, built fresh on the server.
- Client signs. The client submits their name and ticks the agree box. The server validates a non-empty name and the agree flag, then records the audit trail and flips the status to
signed. - After signing (detached). The server returns the response to the client first, then finishes the slower work in the background: it emails the photographer a “signed” notification and archives a copy to Google Drive.
A signed contract is locked: the server rejects edits with a “locked” error once the status is signed.
Where contracts are stored
Each contract is stored server-side in ShootCal’s database, owned by your account. A single contract holds everything about that agreement:
- The signing-link token (unique to that contract) and its type (wedding, family, or custom).
- The client/date/package details: title, client name and email, date, and location. Historical rows may retain an existing event identifier, but new contract creation does not promise a durable calendar-booking link.
- Ordered line items, each with a description, whole-number quantity, exact unit price in cents, and exact extended amount, plus the contract total and retainer/deposit.
- The rich terms body, your studio name, and your typed photographer signature.
- The status (
draft,sent, orsigned). - The e-signature audit trail: the signer’s name, their IP, the browser they used, and the timestamps for when it was created, sent, and signed.
- Pointers to the Google Drive archive copy, once one has been made.
New itemized contracts use exact integer cents and whole-number quantities, matching invoices. The server independently checks every extended amount and total before saving. Older contracts keep their historical plain-text price and additional-hours fields unchanged: their saved price already included those extras, so ShootCal does not split or backfill them and cannot double-count them. Fractional values on those older contracts remain exactly as stored. Per-user isolation is enforced throughout, so every read and write is scoped to your account.
The signing token and the public link
On contract creation the server generates a long, cryptographically random token (a CSPRNG-style value, base64url-encoded to roughly 24 URL-safe characters, about 144 bits of entropy), so it is not practically guessable. It is stored with the contract and must be unique.
The link is https://api.shootcal.com/contract/<token>. It is served directly by the API host.
The token is the only credential for the public page; there is no login. Before any lookup, the server first checks that the token has the expected shape, then finds the matching contract. Both the page route and the sign route are public (no login), so the client can open and sign without an account.
Building the template: the rich editor
The editor is a self-hosted rich-text editor, served from ShootCal’s own site rather than a third-party CDN.
The toolbar is intentionally minimal: headings (levels 1/2/3), bold, italic, links, bullet and numbered lists, and “clear formatting.” Links are checked before they are accepted, and the server sanitizer independently keeps only safe http(s) or mailto addresses.
An empty document is normalized to an empty string. A legacy plain-text or markdown-lite master template is upconverted to HTML when you open the template editor, so older templates can still be edited with their formatting intact. Existing flat-price contract artifacts created before ShootCal stored a token-aware merge source are treated more conservatively: they remain preview/send-only with their original pricing and agreement text together. Native itemized contracts are not frozen merely because provenance has been cleared. Create a new contract to replace a frozen flat-price artifact with editable invoice-style items; ShootCal does not guess at or silently rewrite the legal text of an older artifact.
Merge tokens are inserted as plain text at the cursor. The available tokens are surfaced as clickable chips: {client}, {email}, {date}, {location}, {packageName}, {package}, {price}, {deposit}, {restrictions}, {photographer}, {today}. Older saved templates that already contain {additionalHours} still resolve it for compatibility, but new templates do not offer that legacy token because additional hours now appear in the services table.
Generating a contract: services, quantities, total, and retainer
Products and services are reusable presets. Picking one adds a client-visible row with its description, quantity, and unit price; you can add, edit, or remove more rows. Record overtime as a row such as Additional hours · 2 · $150.00, rather than in a separate hidden field. A line item can also have a $0.00 unit price, so something you include at no charge (an engagement session, a print credit) shows on the agreement as a real row without changing the total. The Total is the exact sum of quantity × unit price for every row, and the server repeats that calculation before accepting the contract. The retainer/deposit stays separate from the item total.
Contracts use the same line-items section as invoices. Picking a saved product adds a row with its name and price, and its saved description lands in the Product or service details box beneath the picker, where you can edit it freely. That details text prints once under the services table on the agreement: lines that start with a bullet or a dash become a list, and blank lines separate paragraphs. You can also create a new product or service without leaving the editor. Row descriptions are capped at 300 Unicode characters and the details box at 8,000. Older contracts whose rows still carry flattened details keep rendering those bullets as an indented list.
The {placeholders} are resolved in the browser before the body is saved: {client}, {package}, {price}, {deposit}, {restrictions}, {today}, and so on. In the current/default template, the primary package token block becomes exactly one itemized table inside 1. Scope of Services, so the services are not printed twice. A custom template can still place {package} elsewhere and retain its safe rich formatting. Custom or legacy agreement text without the itemized insertion point keeps one fallback Services table before the terms rather than being rewritten. The server-rendered preview, public signing page, signed copy, and Drive PDF all use the same escaped table and total.
How the rich body is sanitized
Because the signing page is public and the body is text you authored, the body is run through a built-in allowlist sanitizer before it ever reaches the page or the Drive archive. It is small and dependency-free, built on PHP’s built-in DOM parser.
- Allowed tags:
p, br, strong, em, b, i, h1, h2, h3, ul, ol, li, a(the rich tags the toolbar can produce, plus the inline tags those map to, plusafor links that arrive via paste). - Allowed attributes: only the link address on
<a>, and onlyhttp(s)ormailtoaddresses (control characters and whitespace are stripped to defeat tricks like a disguisedjavascript:, and any other scheme is rejected). Safe external links getrel="noopener nofollow"andtarget="_blank". - Dropped whole (contents and all):
script, style, iframe, object, embed, noscript, template, svg, math, link, meta, title, head, form, input, button, textarea, select, option, audio, video, source. - Unwrapped (wrapper removed, safe children kept): any other unknown-but-not-dangerous tag like
div,span,table,blockquote. Comments are stripped. Every attribute except a validated link address is removed (nostyle=, noclass=, noon*handlers). - Text is preserved and re-encoded safely, so raw
</&in text cannot break out into markup.
If the body contains block-level HTML it goes through the sanitizer; older plain-text or markdown-lite bodies fall through to a legacy renderer. This is a lazy migration: there is no backfill, and both formats keep rendering.
The client e-signature and the audit trail
The signing page is built on the server. Its styling and small script are inline; the handwritten signature typeface is loaded from the ShootCal API host rather than a third-party CDN. It shows the studio logo (if set), title, a details box (Client / Date / Location / Total / Retainer-Deposit), the sanitized terms with the itemized table in Scope of Services (or the safe fallback table before custom terms), the disclaimer, the photographer’s typed signature, and the sign form. Historical flat-price contracts keep their original Package / Total summary instead of being rewritten into line items.
On submit, the server records:
- The signer’s name (the typed name).
- The signer’s IP. It is taken only from Cloudflare’s
CF-Connecting-IPheader and then the real connecting address; a client-suppliedX-Forwarded-Foris deliberately NOT trusted (so a signer cannot spoof or later repudiate the recorded IP). - The browser they used (the User-Agent).
- The signing timestamp, taken from the server.
This is built in the style of an ESIGN/UETA audit trail. The signature write only lands if the contract is not already signed, which closes the double-sign race: only the first writer lands, and a loser gets back the winning signer’s name and time rather than overwriting the recorded trail.
Security protections on the signing page: it cannot be embedded in a frame (no clickjacking a signature), it is marked noindex so a leaked token URL is not indexed, and it is never cached. The privacy line on the page states the IP is kept until three years after the session date, then deleted.
The Google Drive archive
After a successful sign, ShootCal saves a copy to the photographer’s own Google Drive (best-effort, run in the background after the client’s request returns). It:
- Gets a fresh access token for the photographer; if there is none, it bails quietly. 2. Ensures a folder named “ShootCal Contracts” (searches for it, creates it if missing). If the folder cannot be ensured, it skips rather than dropping the file in your Drive root, leaving the archive link empty so a future re-archive can retry. 3. Builds a clean print-style PDF (title, studio, summary, itemized services for newer contracts, sanitized terms, and both signatures with the signed date) and uploads it. 4. On success, it stores the archive’s file ID and view link back on the contract.
The Drive copy uses the drive.file scope and is explicitly a convenience archive, not a replacement: the copy stored server-side in ShootCal’s database remains the durable, owned copy. Failures are logged and swallowed, so a Drive outage never blocks or unwinds a signature.
Status tracking and the disclaimer
Three statuses drive everything: draft, sent, and signed. Current itemized drafts and sent contracts are editable; older flat-price artifacts without tracked merge provenance stay read-only so their agreement body cannot drift from their historical pricing. Signed contracts are locked. The web app renders the statuses as Draft (grey), Sent (amber), and Signed (green), and the server refuses every edit once a contract is signed. The contracts list shows the date each signed contract was signed, and you can sort it by Recent activity (the default) or Recently signed; signed contracts sort newest-first and the sort you pick is remembered.
The disclaimer is non-negotiable surface area: it is returned alongside the templates so the editor can show it and rendered on every public signing page. It states plainly that ShootCal is not a law firm, the templates are a convenience and not legal advice, and the photographer is solely responsible for the terms. It is page-level guidance, not part of the photographer’s archived agreement.
FAQ
Where exactly is a contract stored, and is the Google Drive copy the source of truth?
The source of truth is the contract stored server-side in ShootCal’s database (one per contract, owned by your account). The Google Drive copy is a best-effort PDF archive created only after signing, written to a “ShootCal Contracts” folder, with its file ID and view link stored back on the contract. The database copy is the durable, owned archive and Drive is not a replacement: if the Drive write fails, the signature still stands and the archive link is just left empty for a possible retry.
What is the signing-link token and how guessable is it?
It is a cryptographically random token (roughly 24 URL-safe characters, about 144 bits of entropy), stored uniquely with the contract, so it is not practically guessable. The public route also checks the token’s shape before any lookup, and the page is marked noindex so a leaked URL will not be indexed.
Can the rich-text body inject script onto the public signing page?
No. Every body is passed through a built-in allowlist sanitizer before it reaches either the public sign page or the Drive archive. Only p, br, strong, em, b, i, h1-h3, ul, ol, li, a survive; script/style/iframe/form/input and similar are removed whole; all attributes except a checked, safe link address on <a> are stripped (so no style=, class=, or on* handlers); javascript: and data: URLs are rejected; and text is re-encoded so raw </& cannot break out. The editor’s Link control applies the same safe-address policy, and the sanitizer remains the backstop for markup that arrives through paste or an older body.
What is recorded as the e-signature, and how is the IP protected from spoofing?
On sign, the server stores the signer’s typed name, their IP, the browser they used (User-Agent), and the signing time, built in the style of an ESIGN/UETA audit trail. The IP is taken only from Cloudflare’s CF-Connecting-IP header (set at the edge) and then the real connecting address; a client-supplied X-Forwarded-For is intentionally ignored so a signer cannot spoof or later repudiate the recorded IP. The signing page tells the signer their name, the timestamp, and IP are recorded, and that the IP is kept until three years after the session date.
What stops a contract from being signed twice or edited after signing?
Two guards. The signature write only lands if the contract is not already signed, so under a concurrent double-tap only the first writer lands; the loser gets the winning signer’s recorded name and time back instead of overwriting the audit trail. Editing is blocked server-side: the server returns a “locked” error once the status is signed, and the status flows draft -> sent -> signed with the badge reflecting it.
How does ShootCal calculate contract line items and totals?
Each new line item has a description, whole-number quantity, and exact unit price in cents. The browser calculates each extended amount and the overall total, and the server independently verifies the same math before storing it. Additional hours are a visible line item. Historical contracts stay on their original flat-price representation (including fractional additional-hour values), so their already-inclusive totals are never split, rounded, or double-counted.
Is the editor loaded from a CDN, and what happens to old plain-text contracts?
The editor is self-hosted, served from ShootCal’s own site rather than a CDN. Old master templates authored before the rich editor were plain text or markdown-lite and are upconverted when opened in the template editor. On the server, a body with block HTML is sanitized, while plain-text contract bodies still use the legacy renderer. Existing flat-price artifacts without tracked merge provenance remain preview/send-only so editing other fields cannot make their historical legal text contradictory.
Does ShootCal email the signing link to the client automatically?
Yes, but only after you open Preview, Save & Send, review the exact preview, and choose Save & Send inside it. ShootCal saves that reviewed snapshot and sends the signing link through your connected Gmail account. You can save the reviewed snapshot as a draft instead, and resending an already-sent contract requires confirmation. ShootCal also emails the photographer after the client signs.
Is this a legally binding contract / legal advice?
ShootCal is explicit that it is not. A disclaimer is returned with the templates and shown on every public signing page: ShootCal is not a law firm, the built-in templates are a convenience and not legal advice, and the photographer is solely responsible for reviewing and adapting the terms. The typed-name and checkbox flow with the IP, browser, and timestamp audit trail is built in the ESIGN/UETA style, but enforceability of the terms themselves is the photographer’s responsibility. ← Back to Help Center