Skip to main content

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:

  1. Log in to Smart Waiver dashboard
  2. Navigate to Settings > API
  3. Generate API key
  4. Copy template ID from waiver template
  5. 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:

  1. User redirected to Smartwaiver platform
  2. Reviews pre-filled waiver
  3. Signs electronically (mouse/touch/keyboard)
  4. Smartwaiver validates signatures
  5. Waiver stored in Smartwaiver system
  6. 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:

  1. Generate first waiver URL
  2. Store pending waiver queue in session
  3. After first waiver signed (webhook), check queue
  4. If more waivers needed, redirect to next waiver
  5. Repeat until all required waivers signed
  6. 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_date within 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 VerifyCsrfToken middleware 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

  1. Verify Webhook Signatures - Always validate HMAC signatures
  2. HTTPS Required - Use SSL for all waiver pages
  3. Expiration Policy - Enforce annual waiver renewal
  4. Data Privacy - Securely store waiver data, comply with GDPR/CCPA
  5. Audit Trail - Log all waiver-related actions
  6. Access Control - Restrict Nova waiver viewing to authorized staff