Digital Waivers (Smartwaiver)
Complete integration guide for electronic waiver signing via Smartwaiver platform.
Overview
Lancaster Archery Academy uses Smartwaiver for PCI-compliant electronic waiver management. Smartwaiver provides:
- Electronic signature capture (legally binding)
- Guardian signatures for minors
- Auto-fill participant data
- Webhook notifications on signing
- Template management
- Waiver storage and retrieval
Configuration
Environment Variables
SMARTWAIVER_API_KEY=your_api_key_here
SMARTWAIVER_TEMPLATE_ID=your_template_id
SMARTWAIVER_WEBHOOK_SECRET=your_webhook_secret
Obtaining Credentials:
- Log in to Smart Waiver dashboard
- Navigate to Settings > API
- Generate API key
- Copy template ID from waiver template
- Configure webhook URL and secret
Nova Settings
Settings Page (optimistdigital/nova-settings):
nova_get_settings([
'smartwaiver_api_key',
'smartwaiver_template_id',
])
Admins can update via Nova Settings without touching .env.
Waiver Generation Flow
WaiverManager Service
Location: app/Models/WaiverManager.php
Step 1: Detect Waiver Requirements
On Checkout:
// Check if any accounts need waivers
$accountsMissingWaiver = $user->accountsMissingWaiver();
if ($accountsMissingWaiver->count() > 0) {
// Generate waiver before completing order
$waiverData = (new WaiverManager())->generate(
$user,
$accountsMissingWaiver->pluck('id')->toArray()
);
// Redirect to Smartwaiver
return redirect($waiverData['url']);
}
Account Waiver Status:
// accounts table fields:
- has_waiver (boolean) — Waiver signed
- waiver_signed_date (datetime) — When signed
- waiver_id (varchar) — Smartwaiver waiver ID
// Check if waiver needed
public function needsWaiver(): bool
{
return $this->has_waiver == 0;
}
Step 2: Generate Smartwaiver URL
WaiverManager::generate() Method:
public function generate(\App\Models\User $user, array $accountIds, $args = []): array
{
// Get accounts missing waivers
$accountsMissingWaiver = $user->accounts()
->whereIn('id', $accountIds)
->where('has_waiver', 0)
->get();
if ($accountsMissingWaiver->count() === 0) {
return ['url' => null, 'account_ids' => []];
}
// Prepare Smartwaiver template data
$data = new SmartwaiverTemplateData();
// Get primary account (guardian if minors present)
$adult = $user->primaryAccount();
$minors = $accountsMissingWaiver->filter(function($account) use ($accountIds) {
return $account->isMinor() && in_array($account->id, $accountIds);
});
$adultIsParticipant = in_array($adult->id, $accountIds);
// Set guardian info if minors present
if ($minors->count() > 0) {
$data->setGuardian(
$adult->first_name,
$adult->last_name,
null, // middle name
null, // suffix
$adult->mobile_phone,
null, // relationship (determined by Smartwaiver)
$adult->birthdate->format('Y-m-d'),
$adultIsParticipant // guardian also participating
);
}
// Add all participants
foreach ($accountsMissingWaiver as $account) {
$data->addParticipant(
$account->first_name,
$account->last_name,
null, // middle name
null, // suffix
null, // phone (optional for participants)
$account->birthdate->format('Y-m-d')
);
}
// Set address fields (auto-filled from guardian)
$data->addressLineOne = $adult->address_1;
$data->addressLineTwo = $adult->address_2;
$data->addressCity = $adult->city;
$data->addressState = $adult->state;
$data->addressZip = $adult->zipcode;
$data->addressCountry = $adult->country;
$data->email = $adult->email;
// Get Smartwaiver configuration
$settings = nova_get_settings([
'smartwaiver_template_id',
'smartwaiver_api_key'
]);
// Initialize Smartwaiver SDK
$sw = new Smartwaiver($settings['smartwaiver_api_key']);
// Generate waiver URL
$url = $sw->getWaiverUrl(
$settings['smartwaiver_template_id'],
$data
);
return [
'url' => $url,
'account_ids' => $accountIds
];
}
Step 3: User Signs Waiver
Process:
- User redirected to Smartwaiver platform
- Reviews pre-filled waiver
- Signs electronically (mouse/touch/keyboard)
- Smartwaiver validates signatures
- Waiver stored in Smartwaiver system
- Webhook fired to Lancaster Archery
Step 4: Webhook Processing
Route: routes/web.php
Route::post('/webhooks/smartwaiver', [WaiverWebhookController::class, 'handle'])
->name('webhooks.smartwaiver');
Controller: app/Http/Controllers/WaiverWebhookController.php
public function handle(Request $request)
{
// Verify webhook signature
$signature = $request->header('X-Smartwaiver-Signature');
$expectedSignature = hash_hmac(
'sha256',
$request->getContent(),
config('services.smartwaiver.webhook_secret')
);
if (!hash_equals($expectedSignature, $signature)) {
Log::warning('Invalid Smartwaiver webhook signature');
return response()->json(['error' => 'Invalid signature'], 403);
}
// Parse webhook payload
$payload = $request->json();
$waiverId = $payload->get('waiverId');
$event = $payload->get('event'); // 'create', 'update', 'delete'
if ($event === 'create') {
// Fetch full waiver data from Smartwaiver API
$sw = new Smartwaiver(config('services.smartwaiver.api_key'));
$waiver = $sw->getWaiver($waiverId);
// Extract participant data
$participants = $waiver->getParticipants();
foreach ($participants as $participant) {
// Find matching account by name and birthdate
$account = Account::where('first_name', $participant->getFirstName())
->where('last_name', $participant->getLastName())
->where('birthdate', $participant->getDob())
->first();
if ($account) {
// Update account waiver status
$account->update([
'has_waiver' => 1,
'waiver_signed_date' => now(),
'waiver_id' => $waiverId,
]);
Log::info("Waiver signed for account {$account->id}");
}
}
// Store waiver record
Waiver::create([
'smartwaiver_id' => $waiverId,
'template_id' => $waiver->getTemplateId(),
'title' => $waiver->getTitle(),
'signed_date' => $waiver->getCreatedOn(),
'content' => json_encode($waiver),
]);
}
return response()->json(['success' => true]);
}
Tournament-Specific Waivers
Division Requirements
Configuration:
divisions (
id, name, org, age_start, age_end, gender,
req_us_archery_num (boolean),
waivers (JSON) — ['template_id_1', 'template_id_2']
)
Example:
{
"waivers": [
"5f3a2b1c4d5e6f7a8b9c0d1e", // General liability waiver
"6g4b3c2d5e6f7a8b9c0d1e2f" // USA Archery tournament waiver
]
}
Validation
Before Tournament Registration:
public function canRegisterForDivision(Account $account, Division $division): bool
{
// Check age requirements
if (!$this->meetsAgeRequirement($account, $division)) {
return false;
}
// Check gender requirements
if ($division->gender && $account->gender !== $division->gender) {
return false;
}
// Check US Archery number if required
if ($division->req_us_archery_num && !$account->us_archery_number) {
return false;
}
// Check required waivers
if ($division->waivers) {
foreach ($division->waivers as $templateId) {
// Check if account has signed this waiver template
$hasSigned = $account->waivers()
->where('template_id', $templateId)
->where('signed_date', '>', now()->subYear()) // Within last year
->exists();
if (!$hasSigned) {
return false;
}
}
}
return true;
}
Error Messages:
- "You must be between X and Y years old"
- "This division is for [gender] participants only"
- "US Archery number required for this division"
- "Additional waiver required: [waiver name]"
Multi-Waiver Flow
If Multiple Waivers Needed:
- Generate first waiver URL
- Store pending waiver queue in session
- After first waiver signed (webhook), check queue
- If more waivers needed, redirect to next waiver
- Repeat until all required waivers signed
- Complete registration
Waiver Management
Viewing Signed Waivers
Account Dashboard:
// Display waiver status
@if($account->has_waiver)
<div class="text-green-600">
✓ Waiver signed on {{ $account->waiver_signed_date->format('M d, Y') }}
</div>
<a href="{{ route('account.waiver.view', $account) }}" class="text-blue-600">
View Signed Waiver
</a>
@else
<div class="text-red-600">
✗ Waiver not signed
</div>
@endif
Nova Resource (app/Nova/Waiver.php):
- List all signed waivers
- Search by participant name
- Filter by template, date range
- View waiver PDF link
- Resend waiver email action
Waiver Expiration
Policy: Waivers expire after 1 year
Validation:
public function hasValidWaiver(): bool
{
if (!$this->has_waiver) {
return false;
}
return $this->waiver_signed_date->isAfter(now()->subYear());
}
// On registration, check validity
if (!$account->hasValidWaiver()) {
return redirect()->route('waiver.generate', ['accounts' => [$account->id]]);
}
Renewal Reminders:
- Email sent 30 days before expiration
- Email sent on expiration day
- Account flagged in Nova
Waiver Templates
Smartwaiver Dashboard:
- Create/edit waiver templates
- Add custom fields (checkboxes, initials, text)
- Configure signature requirements
- Set auto-email confirmations
- Add PDF watermarks
Template Variables:
{{participant_name}}{{participant_dob}}{{guardian_name}}{{address}}{{email}}{{phone}}{{date}}
Linking Waivers to Reservations
Database Structure
reservation_waiver (pivot table)
reservation_id → reservations.id
waiver_id → waivers.id
Association Logic
On Reservation Creation:
// After creating reservation
$reservation = Reservation::create([...]);
// Link signed waivers
$account = $reservation->account;
if ($account->has_waiver) {
$waiver = Waiver::where('smartwaiver_id', $account->waiver_id)->first();
if ($waiver) {
$reservation->waivers()->attach($waiver->id);
}
}
Displaying Linked Waivers
Nova Reservation Resource:
BelongsToMany::make('Waivers')
->fields(function () {
return [
Text::make('Signed Date', 'signed_date')->sortable(),
Text::make('Template', 'title'),
];
})
->readonly(),
Smartwaiver SDK Integration
Installation
Custom Fork in lib/smartwaiver-php-sdk/
Autoload via composer.json:
{
"autoload": {
"psr-4": {
"Smartwaiver\\": "lib/smartwaiver-php-sdk/src/"
}
}
}
SDK Usage Examples
Initialize Client:
use Smartwaiver\Smartwaiver;
$sw = new Smartwaiver(config('services.smartwaiver.api_key'));
Get Waiver Templates:
$templates = $sw->getWaiverTemplates();
foreach ($templates as $template) {
echo $template->getTemplateId();
echo $template->getTitle();
echo $template->getPublishedVersion();
}
Retrieve Specific Waiver:
$waiver = $sw->getWaiver($waiverId);
echo $waiver->getTitle();
echo $waiver->getCreatedOn()->format('Y-m-d H:i:s');
foreach ($waiver->getParticipants() as $participant) {
echo $participant->getFirstName();
echo $participant->getLastName();
echo $participant->getDob();
}
Search Waivers:
// Get waivers from last 30 days
$waivers = $sw->getWaiverSummaries(
30, // days
true // verified only
);
foreach ($waivers as $summary) {
echo $summary->getWaiverId();
echo $summary->getTitle();
}
Get Waiver Photos:
$waiver = $sw->getWaiver($waiverId, true); // Include photos
foreach ($waiver->getPhotos() as $photo) {
$photoType = $photo->getType(); // 'signature', 'initials', 'photo'
$photoData = $photo->getPhoto(); // Base64 encoded image
// Save to file or display
file_put_contents("waiver_{$photoType}.png", base64_decode($photoData));
}
Testing
Sandbox Mode
Smartwaiver Test Mode:
- Use test template ID
- Waivers not legally binding
- No email confirmations sent
- Unlimited test submissions
Local Testing
ngrok for Webhooks:
# Expose local server
ngrok http 8000
# Configure webhook URL in Smartwaiver:
https://abc123.ngrok.io/webhooks/smartwaiver
Manual Waiver Signing:
// Artisan command for testing
php artisan waiver:test-sign {account_id}
// Manually updates account waiver status without Smartwaiver
Troubleshooting
Common Issues
Waiver Not Detected After Signing:
- Check webhook delivery in Smartwaiver dashboard
- Verify webhook secret matches
- Review webhook logs:
storage/logs/laravel.log - Check participant name/birthdate match exactly
"Waiver Required" Error Despite Signing:
- Check
accounts.has_waiver= 1 - Verify
waiver_signed_datewithin last year - Confirm waiver template ID matches requirement
- Clear cache:
php artisan cache:clear
Webhook 403 Forbidden:
- CSRF token issue (webhooks excluded from CSRF)
- Signature verification failing
- Check
VerifyCsrfTokenmiddleware exception - Verify webhook secret in
.env
Duplicate Accounts in Waiver:
- Smartwaiver creates separate entries for each participant
- Match by first_name + last_name + birthdate
- Handle middle names/suffixes carefully
Debugging Webhooks
Log Incoming Webhooks:
public function handle(Request $request)
{
Log::info('Smartwaiver webhook received', [
'payload' => $request->all(),
'headers' => $request->headers->all(),
]);
// ... rest of processing
}
Test Webhook Manually:
curl -X POST http://lancasterarcheryacademy.test/webhooks/smartwaiver \
-H "Content-Type: application/json" \
-H "X-Smartwaiver-Signature: test_signature" \
-d '{"event":"create","waiverId":"test_waiver_id"}'
Security Considerations
- Verify Webhook Signatures - Always validate HMAC signatures
- HTTPS Required - Use SSL for all waiver pages
- Expiration Policy - Enforce annual waiver renewal
- Data Privacy - Securely store waiver data, comply with GDPR/CCPA
- Audit Trail - Log all waiver-related actions
- Access Control - Restrict Nova waiver viewing to authorized staff