Skip to main content

Overview

The Email API allows you to send confirmation emails to attendees with personalized content, QR codes, and portal links. Emails are sent via the Resend service using edge functions.

Send Confirmation Emails

Send confirmation emails to one or more attendees:
const { data, error } = await supabase.functions.invoke('send-confirmation-email', {
  body: {
    attendee_ids: [
      '123e4567-e89b-12d3-a456-426614174001',
      '456e7890-e89b-12d3-a456-426614174002'
    ]
  }
})

if (error) {
  console.error('Error sending emails:', error)
} else {
  console.log('Emails sent:', data.sent)
  console.log('Emails failed:', data.failed)
}

Request Body

Response

{
  "results": [
    {
      "id": "123e4567-e89b-12d3-a456-426614174001",
      "success": true
    },
    {
      "id": "456e7890-e89b-12d3-a456-426614174002",
      "success": true
    }
  ],
  "sent": 2,
  "failed": 0
}
results
array
Array of result objects for each attendee
sent
number
Total number of emails successfully sent
failed
number
Total number of emails that failed to send

Test Emails

Send a test email to verify your template before sending to attendees:
const { data, error } = await supabase.functions.invoke('send-confirmation-email', {
  body: {
    attendee_ids: ['123e4567-e89b-12d3-a456-426614174001'],
    test_email_override: '[email protected]'
  }
})

if (!error) {
  console.log('Test email sent to [email protected]')
}
When test_email_override is provided, all emails will be sent to that address instead of the attendee’s actual email. The subject line will be prefixed with “[TEST]”.

Email Template Variables

Customize email content using template variables in your event’s confirmation_email_content:

Email Template Example

<h2>Hi {{name}},</h2>
<p>You're confirmed for <strong>{{event_name}}</strong>!</p>

<p>
  <strong>Date:</strong> {{event_date}}<br/>
  <strong>Venue:</strong> {{event_venue}}
</p>

<p>
  Your unique check-in ID is: 
  <strong style="font-family: monospace; font-size: 18px; letter-spacing: 2px;">
    {{unique_id}}
  </strong>
</p>

<p>Show this QR code at the entrance for quick check-in:</p>
<p>
  <img src="{{qr_code_url}}" alt="QR Code" width="200" height="200" />
</p>

<p>
  <a href="{{portal_url}}" style="display: inline-block; padding: 12px 24px; background: #0066cc; color: white; text-decoration: none; border-radius: 4px;">
    View Your Attendee Portal →
  </a>
</p>

<p>See you there!</p>

Set Email Template

Update the email template for an event:
const emailTemplate = `
  <h2>Hi {{name}},</h2>
  <p>You're confirmed for <strong>{{event_name}}</strong>!</p>
  <p><strong>Date:</strong> {{event_date}}<br/><strong>Venue:</strong> {{event_venue}}</p>
  <p>Your unique check-in ID is: <strong style="font-family: monospace; font-size: 18px; letter-spacing: 2px;">{{unique_id}}</strong></p>
  <p>Show this QR code at the entrance for quick check-in:</p>
  <p><img src="{{qr_code_url}}" alt="QR Code" width="200" height="200" /></p>
  <p><a href="{{portal_url}}">View your attendee portal →</a></p>
  <p>See you there!</p>
`

const { error } = await supabase
  .from('events')
  .update({ confirmation_email_content: emailTemplate })
  .eq('id', eventId)

if (error) {
  console.error('Error updating template:', error)
} else {
  console.log('Email template updated')
}

Bulk Email Sending

Send emails to all attendees who haven’t received one:
// Get attendees without confirmation emails
const { data: attendees } = await supabase
  .from('attendees')
  .select('id')
  .eq('event_id', eventId)
  .eq('confirmation_email_sent', false)

if (attendees && attendees.length > 0) {
  const attendeeIds = attendees.map(a => a.id)
  
  // Send emails in batches of 50
  const batchSize = 50
  for (let i = 0; i < attendeeIds.length; i += batchSize) {
    const batch = attendeeIds.slice(i, i + batchSize)
    
    const { data, error } = await supabase.functions.invoke('send-confirmation-email', {
      body: { attendee_ids: batch }
    })
    
    if (error) {
      console.error(`Batch ${i / batchSize + 1} failed:`, error)
    } else {
      console.log(`Batch ${i / batchSize + 1}: ${data.sent} sent, ${data.failed} failed`)
    }
    
    // Wait 1 second between batches to avoid rate limits
    if (i + batchSize < attendeeIds.length) {
      await new Promise(resolve => setTimeout(resolve, 1000))
    }
  }
}

Email Status Tracking

Check which attendees have received confirmation emails:
const { data: stats } = await supabase
  .from('attendees')
  .select('confirmation_email_sent')
  .eq('event_id', eventId)

if (stats) {
  const sent = stats.filter(a => a.confirmation_email_sent).length
  const pending = stats.length - sent
  
  console.log(`Emails sent: ${sent} / ${stats.length}`)
  console.log(`Pending: ${pending}`)
}

Get Attendees Without Emails

Retrieve attendees who haven’t received confirmation emails:
const { data: pending } = await supabase
  .from('attendees')
  .select('id, name, email, unique_id')
  .eq('event_id', eventId)
  .eq('confirmation_email_sent', false)

if (pending) {
  console.log(`${pending.length} attendees pending email`)
  pending.forEach(a => {
    console.log(`${a.name} (${a.email})`)  
  })
}

Email Audit Logging

The send-confirmation-email function automatically logs email sending activity:
// Audit logs are created automatically
// View them from the audit_logs table
const { data: logs } = await supabase
  .from('audit_logs')
  .select('*')
  .eq('action', 'sent_confirmation_emails')
  .eq('entity_type', 'event')
  .eq('entity_id', eventId)
  .order('created_at', { ascending: false })

if (logs) {
  logs.forEach(log => {
    const details = log.details as any
    console.log(`${log.created_at}: Sent ${details.sent}, Failed ${details.failed}`)
  })
}

Email Layout

Emails are automatically wrapped in a responsive layout with your organization branding:
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body style="margin:0; padding:0; background-color:#f4f4f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
  <div style="max-width:600px; margin:0 auto; padding:24px;">
    <div style="background:#ffffff; border-radius:0; padding:32px; border:1px solid #e4e4e7;">
      <!-- Your email content here -->
    </div>
    <div style="text-align:center; padding:16px; color:#71717a; font-size:12px;">
      <p>Your Organization — Powered by PassTru</p>
    </div>
  </div>
</body>
</html>

QR Code Generation

QR codes are generated using the QR Server API:
const uniqueId = 'K7M9P2Q5'
const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(uniqueId)}`

// This URL is automatically included in emails as {{qr_code_url}}

Error Handling

Common email sending errors:
  • Unauthorized: User doesn’t have access to the event
  • Invalid attendee IDs: UUIDs are malformed
  • No attendees found: Attendee IDs don’t exist
  • Resend API error: Service unavailable or rate limited
const { data, error } = await supabase.functions.invoke('send-confirmation-email', {
  body: { attendee_ids: attendeeIds }
})

if (error) {
  console.error('Email sending error:', error.message)
  
  // Check for specific errors
  if (error.message.includes('Unauthorized')) {
    console.error('You do not have permission to send emails for this event')
  } else if (error.message.includes('Invalid attendee_ids')) {
    console.error('One or more attendee IDs are invalid')
  } else {
    console.error('An unexpected error occurred')
  }
} else {
  // Check individual results
  data.results.forEach(result => {
    if (!result.success) {
      console.error(`Failed to send email to attendee ${result.id}: ${result.error}`)
    }
  })
}

Rate Limits

Resend API limits (free tier):
  • 100 emails per day
  • 3,000 emails per month
  • Upgrade to paid plans for higher limits
To avoid rate limits, send emails in batches with delays between batches. See the “Bulk Email Sending” example above.

Security

The email function includes security measures:
  1. Authentication required: User must be signed in
  2. Authorization checks: User must own or be assigned to the event
  3. Input validation: Attendee IDs and email addresses are validated
  4. HTML escaping: User data is escaped to prevent injection
  5. Test mode: Use test_email_override to avoid sending to real users during testing

Next Steps

Attendees API

Manage attendees and their data

Check-ins API

Handle check-ins at your event

Build docs developers (and LLMs) love