April 28, 2026 · Use Case Guide
Electronic Signature Waivers: How to Automate Liability Releases via API
Gyms, adventure parks, event venues, and sports facilities process hundreds of liability waivers every week. Most still use clipboards. Here's how to automate the entire flow with one API call.
Founder, Signbee
TL;DR
Electronic waivers are legally binding in all 50 US states under the ESIGN Act. Replace clipboards with one API call: send the waiver, participant signs on their phone, you get a SHA-256 certified PDF with a complete audit trail. Free waiver template and code example below.
Industries using electronic waivers
| Industry | Common waivers | Volume |
|---|---|---|
| Fitness / Gyms | Membership waivers, class liability | 100-500/mo |
| Adventure sports | Activity releases, equipment rental | 50-200/mo |
| Events / Festivals | Participant waivers, vendor agreements | 500-5,000/event |
| Youth programs | Parental consent, medical authorization | 50-300/season |
| Equipment rental | Rental agreements, damage waivers | 100-1,000/mo |
Free liability waiver template
# Liability Waiver and Release of Claims **Facility:** [BUSINESS_NAME] **Activity:** [ACTIVITY_NAME] **Date:** [DATE] ## Assumption of Risk I, [PARTICIPANT_NAME], acknowledge that participation in [ACTIVITY_NAME] involves inherent risks including but not limited to: [LIST_SPECIFIC_RISKS]. I voluntarily assume all risks associated with participation, whether known or unknown. ## Release of Liability I release [BUSINESS_NAME], its owners, employees, agents, and affiliates from any and all claims, demands, or causes of action arising from my participation, including claims of negligence. ## Indemnification I agree to indemnify and hold harmless [BUSINESS_NAME] from any claims brought by third parties arising from my participation. ## Medical Authorization In the event of injury, I authorize [BUSINESS_NAME] to seek emergency medical treatment on my behalf. **Emergency Contact:** [EMERGENCY_NAME] [EMERGENCY_PHONE] ## Acknowledgment I have read this waiver, understand its terms, and sign it voluntarily. I am at least 18 years of age. By signing below, I agree to all terms above.
Sending a waiver via API
const waiver = generateWaiver({
business: "Summit Adventure Park",
activity: "Rock Climbing & Zip Line",
risks: "falls, rope burns, muscle strains, equipment failure",
participant: "Alex Johnson",
emergencyContact: "Sarah Johnson (555-0123)",
});
const res = await fetch("https://signb.ee/api/v1/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_KEY",
},
body: JSON.stringify({
markdown: waiver,
recipient_name: "Alex Johnson",
recipient_email: "alex@example.com",
expires_in_days: 1, // Waivers are usually time-sensitive
}),
});
const { document_id, signing_url } = await res.json();
// Send signing_url via SMS for on-site signingThe SMS & QR Entry Pipeline
For high-throughput facilities like adventure parks, trampoline arenas, and busy conferences, the registration desk is a primary operational bottleneck. Forcing signers to use physical clipboards causes lengthy delays, while typing long email links on a physical terminal generates administrative friction. The modern standard is a frictionless, mobile-first SMS & QR Entry Pipeline. This architecture shifts the workload entirely to the user's personal device, keeping lines moving and automating gate verification in real time.
Step-by-Step Pipeline Mechanics
The automated self-service pipeline flows through four key stages:
- Initiation: When a user approaches a check-in portal or kiosk terminal, they input their name and phone number. The portal backend makes an authenticated API call to the Signbee API to generate a temporary signing URL.
- SMS Dispatch via Twilio: The backend receives the signing URL and dispatches it to the participant's phone via Twilio SMS: "Hi Alex, please sign the waiver for Summit Adventure Park here to get your entry pass: https://signb.ee/s/xyz". This bypasses email latency and local terminal queuing.
- Webhook and State Mutation: The user reads and signs the waiver directly on their phone. Once completed, Signbee dispatches a cryptographically verified webhook event (secured via SVIX headers) to the developer's backend. The backend updates the participant's database status to
SIGNED. - Dynamic QR Code Generation: Upon updating the database, the backend compiles a secure entry token (representing the signed document ID and timestamp) and generates a raw, inline SVG QR code. This pass is shown on the user's mobile screen or sent as a follow-up link. When scanned at the physical entrance turnstile, the gate scanner validates the token against the local database to grant access.
System Architecture & Data Sequence
To visualize the lifecycle of this pipeline, here is the complete sequence of events from registration to gate verification:
+-------------+ +---------------+ +-------------+ +---------------+ +-------------+ +---------------+
| Participant | | Kiosk/Portal | | Signbee API | | Twilio SMS | | Kiosk DB / | | Entrance Gate |
| (Phone) | | (Kiosk App) | | & Webhooks | | Service | | Backend | | & Scanner |
+-------------+ +---------------+ +-------------+ +---------------+ +-------------+ +---------------+
| | | | | |
| 1. Register details| | | | |
|-------------------->| | | | |
| | 2. Create Doc Req | | | |
| |-------------------->| | | |
| | 3. Return sign_url | | | |
| |<--------------------| | | |
| | | | | |
| | 4. Dispatch SMS Command | | |
| |------------------------------------------>| | |
| 5. SMS sent with Signbee URL | | | |
|<----------------------------------------------------------------| | |
| | | | | |
| 6. Access signing page | | | |
|------------------------------------------>| | | |
| 7. Sign waiver on mobile browser | | | |
|------------------------------------------>| | | |
| | | 8. Webhook: document.signed | |
| | | (SVIX Cryptographic Signature) | |
| | |------------------------------------------>| |
| | | | | |
| | | | 9. Verify SVIX Sig | |
| | | | & Update DB | |
| | | |------------------+ | |
| | | | | | |
| | | | 10. Generate QR | | |
| | | | SVG Pass | | |
| | | |<- - - - - - - - -+ | |
| | 11. Render SVG QR Pass on Kiosk Screen | | |
| |<----------------------------------------------------------------| |
| 12. Also send SVG QR code to email/phone | | | |
|<--------------------------------------------------------------------------------------| |
| | | | | |
| 13. Present QR entry pass to scanner at gate | |
|------------------------------------------------------------------------------------------------------------>|
| | | | | 14. Query status |
| | | | |<--------------------|
| | | | | 15. Status: SIGNED |
| | | | |-------------------->|
| 16. Gate Unlocks & Access Granted | | | |
|<------------------------------------------------------------------------------------------------------------|Cryptographically Verifying Webhook Events
Because webhook payloads are delivered over the public internet, verifying their origin is crucial. Attackers could spoof requests to bypass gate controls without signing the actual liability agreement. Signbee utilizes standard SVIX signatures to establish proof-of-origin and prevent replay attacks. The headers svix-id, svix-timestamp, and svix-signature are sent with every hook. Developers must cryptographically verify these signatures before making any state mutations in the system database.
The signature is computed as an HMAC-SHA256 hash using the hex-stripped webhook secret. Below are production-ready code examples in Node.js (Express) and Python (FastAPI) showing how to securely handle SVIX verification and programmatically return high-performance inline SVG components representing the QR code entry pass.
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.raw({ type: "application/json" }));
const SVIX_SECRET = process.env.SVIX_SECRET || "whsec_xXyYzZ123456789";
// Helper to generate a lightweight, vector-based QR code SVG
function generateQrSvg(token) {
const size = 256;
const grid = 21; // Version 1 QR code grid
const cellSize = size / grid;
let paths = "";
// Compute SHA-256 hash of the entry token to map deterministic blocks
const hash = crypto.createHash("sha256").update(token).digest();
for (let r = 0; r < grid; r++) {
for (let c = 0; c < grid; c++) {
// Finder pattern (top-left)
const isTopLeftFinder = r < 7 && c < 7 && (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4));
// Finder pattern (top-right)
const isTopRightFinder = r < 7 && c >= grid - 7 && (r === 0 || r === 6 || c === grid - 1 || c === grid - 7 || (r >= 2 && r <= 4 && c >= grid - 5 && c <= grid - 3));
// Finder pattern (bottom-left)
const isBottomLeftFinder = r >= grid - 7 && c < 7 && (r === grid - 1 || r === grid - 7 || c === 0 || c === 6 || (r >= grid - 5 && r <= grid - 3 && c >= 2 && c <= 4));
const isFinder = isTopLeftFinder || isTopRightFinder || isBottomLeftFinder;
// Determine dark cells deterministically using hash bytes and row/col indices
const hashIndex = (r * grid + c) % hash.length;
const isDarkCell = (hash[hashIndex] ^ (r + c)) % 2 === 0;
if (isFinder || isDarkCell) {
const x = c * cellSize;
const y = r * cellSize;
paths += `M ${x.toFixed(1)} ${y.toFixed(1)} h ${cellSize.toFixed(1)} v ${cellSize.toFixed(1)} h -${cellSize.toFixed(1)} z `;
}
}
}
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="100%" height="100%" style="background:#fff;padding:16px;border-radius:8px;"><path d="${paths}" fill="#09090b" shape-rendering="crispEdges"/></svg>`;
}
app.post("/webhooks/signbee", (req, res) => {
const payload = req.body.toString();
const headers = req.headers;
const msgId = headers["svix-id"];
const msgTimestamp = headers["svix-timestamp"];
const msgSignature = headers["svix-signature"];
if (!msgId || !msgTimestamp || !msgSignature) {
return res.status(400).json({ error: "Missing SVIX signature headers" });
}
// Prevent replay attacks (5 minute threshold)
const now = Math.floor(Date.now() / 1000);
const diff = Math.abs(now - parseInt(msgTimestamp, 10));
if (diff > 300) {
return res.status(400).json({ error: "Timestamp tolerance exceeded" });
}
// Construct signature base
const toSign = `${msgId}.${msgTimestamp}.${payload}`;
const secretKey = SVIX_SECRET.split("_")[1] || SVIX_SECRET;
const secretBuffer = Buffer.from(secretKey, "base64");
// Compute signature
const expectedSig = crypto
.createHmac("sha256", secretBuffer)
.update(toSign)
.digest("base64");
// Verify signature
const signatureParts = msgSignature.split(" ");
let isValid = false;
for (const part of signatureParts) {
const [version, signatureValue] = part.split(",");
if (version === "v1" && signatureValue === expectedSig) {
isValid = true;
break;
}
}
if (!isValid) {
return res.status(401).json({ error: "Invalid cryptographic signature" });
}
try {
const event = JSON.parse(payload);
if (event.type === "document.signed") {
const { document_id, recipient_email, recipient_name } = event.data;
// Update check-in record in database
// await db.query("UPDATE checkins SET status = 'SIGNED' WHERE document_id = $1", [document_id]);
console.log(`Updating DB: ${document_id} signed by ${recipient_name}`);
// Generate secure gate verification token
const entryToken = crypto
.createHmac("sha256", "gate-encryption-key")
.update(`${document_id}:${msgTimestamp}`)
.digest("hex");
const qrSvg = generateQrSvg(entryToken);
// Return the raw SVG component representing the entry QR code
res.setHeader("Content-Type", "image/svg+xml");
return res.status(200).send(qrSvg);
}
return res.status(200).json({ status: "received" });
} catch (error) {
return res.status(500).json({ error: "Internal processing error" });
}
});from fastapi import FastAPI, Request, Response, HTTPException, status
from fastapi.responses import JSONResponse
import hmac
import hashlib
import base64
import time
import json
app = FastAPI()
SVIX_SECRET = "whsec_xXyYzZ123456789"
def generate_qr_svg(token: str) -> str:
# Programmatic lightweight generation of QR-style SVG path.
# Finder patterns + deterministic grid computed from token hash.
size = 256
grid = 21
cell_size = size / grid
paths = []
# Generate stable grid from SHA256 hash of entry token
token_hash = hashlib.sha256(token.encode('utf-8')).digest()
for r in range(grid):
for c in range(grid):
# Top-left finder
is_tl_finder = r < 7 and c < 7 and (r == 0 or r == 6 or c == 0 or c == 6 or (2 <= r <= 4 and 2 <= c <= 4))
# Top-right finder
is_tr_finder = r < 7 and c >= grid - 7 and (r == 0 or r == 6 or c == grid - 1 or c == grid - 7 or (2 <= r <= 4 and grid - 5 <= c <= grid - 3))
# Bottom-left finder
is_bl_finder = r >= grid - 7 and c < 7 and (r == grid - 1 or r == grid - 7 or c == 0 or c == 6 or (grid - 5 <= r <= grid - 3 and 2 <= c <= 4))
is_finder = is_tl_finder or is_tr_finder or is_bl_finder
hash_idx = (r * grid + c) % len(token_hash)
is_dark = (token_hash[hash_idx] ^ (r + c)) % 2 == 0
if is_finder or is_dark:
x = c * cell_size
y = r * cell_size
paths.append(f"M {x:.1f} {y:.1f} h {cell_size:.1f} v {cell_size:.1f} h -{cell_size:.1f} z")
path_data = " ".join(paths)
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}" width="100%" height="100%" '
f'style="background:#fff;padding:16px;border-radius:8px;">'
f'<path d="{path_data}" fill="#09090b" shape-rendering="crispEdges"/>'
f'</svg>'
)
@app.post("/webhooks/signbee")
async def signbee_webhook(request: Request):
# Retrieve SVIX signature verification headers
msg_id = request.headers.get("svix-id")
msg_timestamp = request.headers.get("svix-timestamp")
msg_signature = request.headers.get("svix-signature")
if not msg_id or not msg_timestamp or not msg_signature:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Missing required SVIX verification headers"
)
# Prevent signature replay attacks (within 5 minutes)
try:
ts = int(msg_timestamp)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid svix-timestamp format"
)
if abs(time.time() - ts) > 300:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Webhook request expired (timestamp drift too high)"
)
# Get raw body for cryptographic hashing
body_bytes = await request.body()
body_str = body_bytes.decode("utf-8")
# Format payload base for HMAC verification
to_sign = f"{msg_id}.{msg_timestamp}.{body_str}".encode("utf-8")
# Strip prefix from secret key if needed
secret_key = SVIX_SECRET.split("_")[1] if "_" in SVIX_SECRET else SVIX_SECRET
secret_bytes = base64.b64decode(secret_key)
# Compute the expected HMAC signature
computed_mac = hmac.new(secret_bytes, to_sign, hashlib.sha256).digest()
expected_sig = base64.b64encode(computed_mac).decode("utf-8")
# Check signatures listed in the header (v1 format)
signature_parts = msg_signature.split(" ")
is_valid = False
for part in signature_parts:
if "," in part:
version, sig_val = part.split(",", 1)
if version == "v1" and hmac.compare_digest(sig_val, expected_sig):
is_valid = True
break
if not is_valid:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Signature verification failed"
)
# Payload verified, parse JSON content
try:
payload = json.loads(body_str)
event_type = payload.get("type")
if event_type == "document.signed":
data = payload.get("data", {})
doc_id = data.get("document_id")
recipient_name = data.get("recipient_name")
# Update database status check here (e.g. session.execute)
# update_kiosk_status(db, doc_id, "SIGNED")
# Generate stable gate entry token
entry_token = hmac.new(
b"gate-encryption-key",
f"{doc_id}:{msg_timestamp}".encode("utf-8"),
hashlib.sha256
).hexdigest()
qr_code_svg = generate_qr_svg(entry_token)
return Response(content=qr_code_svg, media_type="image/svg+xml")
return JSONResponse(content={"status": "ignored_event"})
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Payload must be valid JSON"
)Electronic vs paper waivers
Frequently Asked Questions
Are electronic signature waivers legally binding?
Yes — in all 50 US states under the ESIGN Act and UETA. Courts consistently uphold electronically signed waivers when the signer had a clear opportunity to read the terms and the signature is attributable.
Can minors sign electronic waivers?
Minors cannot sign binding waivers — a parent or guardian must sign on their behalf. Include a parent/guardian signature section in your waiver template. Some states further restrict waiver enforceability for minors regardless of parental consent.
What makes a waiver enforceable?
Specific risk identification (not vague catch-alls), clear and conspicuous presentation, voluntary signing with opportunity to read, proper identification of the releasing parties, and a complete audit trail with timestamps.
How do you handle connectivity issues or offline signing for on-site check-in booths?
For physical venues (like trampoline parks or zip-lining tours), internet connectivity drops can stall the check-in line. To build a resilient offline check-in booth, developers should configure local kiosk terminals to run a hybrid service worker that caches the core application UI. When the network is unavailable, the kiosk can capture waiver agreements locally using IndexedDB to store the participant's details, signature paths, and timestamps. However, to maintain legal compliance under the ESIGN Act, these offline records must be cryptographically signed on the local device with a client-side private key and synchronized back to the Signbee cloud via a queue-based synchronization manager immediately when connectivity is restored. Signbee will then retroactively issue the SHA-256 integrity seal using the original offline capture timestamp to preserve the waiver's validity and trigger the respective webhooks to update the access gates.
How does the Signbee API enforce legal waivers across different jurisdictions (e.g., US, UK, EU)?
Enforcing liability releases globally requires adhering to distinct legal frameworks, such as the ESIGN Act and UETA in the United States, the Electronic Communications Act 2000 in the United Kingdom, and the eIDAS regulation in the European Union. In the United States, waivers of negligence are generally enforceable but highly scrutinized, requiring conspicuous risk disclosures. In contrast, under the UK's Unfair Contract Terms Act (UCTA) 1977 and similar EU directives, it is legally impossible to exclude liability for personal injury or death caused by negligence. To manage this programmatically, developers should leverage Signbee’s dynamic template localization flags. The API detects the participant's geographic IP address or explicitly defined locale parameters to inject region-specific severability clauses, mandatory statutory disclosures, and alternate dispute resolution terms, ensuring that the remainder of the waiver remains fully enforceable even if local courts strike down specific liability release clauses.
How does the API verify parental/guardian signatures and ensure consent is legally binding for minors?
Contracts signed by minors are voidable in most jurisdictions. Therefore, liability waivers for participants under the age of majority must be executed by a parent or legal guardian. The Signbee API supports complex multi-party signing workflows designed specifically for family groups. When sending a waiver payload, the developer specifies the signer_type as parent_guardian and includes a secondary metadata object mapping the minor's name, date of birth, and relationship. The signing interface forces the parent to check explicit validation boxes confirming their authority to sign on behalf of the minor, and captures a dual signature input if required. The final generated PDF embeds these relationships in its tamper-proof metadata, linking the guardian's verified email, SMS authentication logs, and IP address directly to the minor's record, creating a robust, legally defensible audit trail that stands up to judicial scrutiny.
Automate liability waivers — 5 free/month, SHA-256 certified.
Last updated: May 29, 2026 · This article is for informational purposes and does not constitute legal advice. Michael Beckett is the founder of Signbee and B2bee Ltd.