beginner
shop-rentals
waivers
signing-links

Issuing a Signing Link

Generate a one-time tokenized URL the rider can open on their phone to sign the rental waiver — no app install required

Levy Fleets TeamMay 7, 20264 min read

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:

  1. Rendered template body with variable substitution (their name, booking number, pickup date, etc.)
  2. Each section as a heading + body block
  3. Acknowledgment checkbox
  4. Typed-name signature field
  5. 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."

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.

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

FieldValue
typed_nameCustomer's full legal name
signature_imageDrawn signature data URL (if provided)
signer_ipIP address from request headers
signer_deviceUser agent string
signed_atUTC timestamp, server-side
rendered_body + rendered_sectionsSnapshot 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].

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.