A signing link is a one-time tokenized URL like /sign/agreement/abc123…
that the rider opens on their phone, browser, or iPad to sign the rental
waiver. Once signed, the token is burned and can't be used again.
Where to issue
Booking detail page
/dashboard/shop-rentals/bookings/[id] → Agreements & Waivers panel.
For each active agreement template, there's a button. Click it to issue a fresh signing link tied to this booking.
Walk-in wizard
Step 4 of the walk-in flow has a "Issue waiver" picker. Selecting a template auto-issues a link that shows on the confirmation screen.
API
POST /api/reservations/[id]/agreements
{
"templateId": "...",
"isPerRider": false,
"expiresInHours": 168
}
Returns:
{
"ok": true,
"signedAgreementId": "...",
"token": "abc123...",
"signingUrl": "/sign/agreement/abc123...",
"expiresAt": "2026-05-14T15:30:00Z"
}
What the customer sees
Opening the link in any browser:
- Rendered template body with variable substitution (their name, booking number, pickup date, etc.)
- Each section as a heading + body block
- Acknowledgment checkbox
- Typed-name signature field
- Submit button
After clicking Submit:
- The signature is captured (typed name + drawn signature image, if provided)
- The IP, user agent, and timestamp are recorded
- The token is burned — can't be reused
- Confirmation screen with timestamp
If they reload the page after signing, they see "Agreement already signed."
Sharing the link
Most operators:
- Walk-in counter — display the signing URL or a QR code on the iPad. The customer scans/types and signs on their phone.
- Pre-pickup email — automated emails (when configured) include the signing link
- Manual SMS — copy the URL and text it to the customer
The link doesn't require account login — possession of the token IS the authorization to sign.
Token expiration
Default: 7 days. Configurable per request via expiresInHours in the
API call.
After expiration, the signing page returns "Link expired (HTTP 410)." The operator must issue a new link.
We use 7 days for typical rental shops — long enough for customers to get to it, short enough to limit forwarded-token risk.
Per-rider links
For group bookings, issue separate links per rider:
POST /api/reservations/[id]/agreements
{
"templateId": "...",
"isPerRider": true,
"riderIndex": 0
}
POST /api/reservations/[id]/agreements
{
"templateId": "...",
"isPerRider": true,
"riderIndex": 1
}
The signing page shows "Rider X of Y" badging when is_per_rider=true.
See Per-rider waivers for the full group waiver flow.
Verifying signatures
After signing, the booking detail page shows a green "Signed" badge with:
- Typed name (the rider's full legal name as typed)
- Signed-at timestamp (UTC)
- Optional PDF download link (some templates auto-generate)
This is your audit record for legal disputes.
What's captured at sign time
| Field | Value |
|---|---|
typed_name | Customer's full legal name |
signature_image | Drawn signature data URL (if provided) |
signer_ip | IP address from request headers |
signer_device | User agent string |
signed_at | UTC timestamp, server-side |
rendered_body + rendered_sections | Snapshot of the template at sign time |
The snapshot is critical — even if you later edit the template, the signed version is preserved.
When to re-issue
If a customer closes the page without signing:
- Wait — the link is still valid
- After a while if they don't get to it, click "Issue signing link" again on the booking detail. The old token is invalidated when a new one is issued for the same template (the new link supersedes).
If a customer signs the wrong template (e.g., adult template signed by parent for minor):
- Delete the wrong signed agreement record (operator-only via SQL)
- Issue a new link for the correct template
Common pitfalls
Customer signs but operator can't see it on the page
Check the booking detail page after a refresh. If still missing, query:
SELECT * FROM signed_agreements
WHERE reservation_id = '<reservation-id>'
ORDER BY created_at DESC;
The row is there if signing succeeded. If not, the signing API may have errored — check Sentry.
Token URL works but signing fails
Usually a transient Stripe/database error. Customer can refresh and
re-sign. If persistent, check the API logs at
POST /api/agreements/sign/[token].
Per-rider links sent to wrong riders
The riderIndex field is just a label — there's no enforcement that
"rider 0 must sign before rider 1." Customers sign in any order. If the
wrong person signs the wrong link, the typed name will reveal the
mismatch in the audit log.
Customer uses a hyphenated / international name
The typed_name field is free-form. Accents, hyphens, apostrophes — all preserved. The only reject is empty / over-200-character names.