TinyKit Pro Docs

Resend Audience API Integration

TinyKit Pro includes a comprehensive integration with Resend's Audience API for advanced contact management, segmentation, and email marketing campaigns.

TinyKit Pro includes a comprehensive integration with Resend's Audience API for advanced contact management, segmentation, and email marketing campaigns.

Overview

This integration automatically syncs your mailing list contacts to Resend, enabling you to:

  • Automatic Contact Sync: New signups are automatically added to Resend
  • Multi-App Safety: Uses app-specific contact properties to prevent conflicts across multiple apps on the same Resend account
  • Rich Contact Metadata: Syncs user data, subscription status, billing information, and more
  • Real-time Updates: Contact properties update automatically when user data changes
  • Segmentation: Leverage contact properties for targeted email campaigns
  • App-Specific Unsubscribe: Users can unsubscribe from your app without affecting other apps on the same Resend account

Architecture

App-Specific Property Namespacing

IMPORTANT: If you use the same Resend account for multiple apps/domains, this integration uses property namespacing to prevent conflicts:

  • Properties are prefixed with your app identifier (e.g., tinykit_subscriptionStatus)
  • Unsubscribing from one app doesn't affect subscriptions in other apps
  • Each app manages its own contact properties independently

Example contact properties in Resend:

email: "user@example.com"
tinykit_subscriptionStatus: "active"
tinykit_accountStatus: "paid"
tinykit_billingPlan: "Professional"
otherapp_subscriptionStatus: "active"
otherapp_accountStatus: "free"

Contact Properties Synced

The following properties are automatically synced for each contact:

PropertyDescriptionExample Values
{app}_subscriptionTypeType of subscriptionnewsletter, waitlist
{app}_subscriptionStatusCurrent subscription statusactive, unsubscribed
{app}_subscribedAtSubscription timestamp2025-01-15T10:30:00Z
{app}_userIdConvex user ID (if authenticated)j57abc123...
{app}_accountStatusUser account statusfree, trial, paid
{app}_userRoleUser role in your appuser, admin
{app}_billingPlanSubscription plan nameProfessional, Enterprise
{app}_billingIntervalBilling cyclemonthly, yearly
{app}_lastActivityAtLast activity timestamp2025-01-15T10:30:00Z

Note: Replace {app} with your app identifier (default: tinykit)

Setup Instructions

1. Configure Environment Variables

Add the following to your Convex environment:

# Set your Resend API key
npx convex env set RESEND_API_KEY "re_your_api_key_here"

# Set your app identifier (for multi-app safety)
npx convex env set RESEND_APP_IDENTIFIER "tinykit"

Important: Use a unique RESEND_APP_IDENTIFIER for each app sharing the same Resend account.

2. Enable Resend Sync in Admin Dashboard

  1. Navigate to Admin → Mailing List Settings
  2. Toggle Enable Resend Sync to ON
  3. (Optional) Enter your Resend Audience ID if you're using audiences
  4. Save settings

3. Initial Data Migration (Optional)

If you have existing contacts in your database, perform a one-time bulk sync:

  1. Go to Admin → Mailing List Settings
  2. Click Bulk Sync Contacts
  3. Wait for the sync to complete
  4. Check sync status in the contacts table

Usage

Automatic Sync Events

Contacts are automatically synced to Resend when:

  1. New Signup: User signs up for newsletter/waitlist
  2. Reactivation: Previously unsubscribed user re-subscribes
  3. Unsubscribe: User unsubscribes (updates app-specific property only)
  4. User Data Changes: User updates profile or subscription status

Manual Operations

Trigger Sync for Single Contact

import { api } from "@/convex/_generated/api";

// In a mutation or action
await ctx.scheduler.runAfter(
  0,
  internal.mailingList.internal.actions.syncContactToResendAction,
  {
    mailingListId: entryId,
    email: "user@example.com",
    status: "active",
    userId: userId, // Optional
    audienceId: "aud_123", // Optional
  },
);

Bulk Sync All Contacts

import { api } from "@/convex/_generated/api";

// In an action
const results = await ctx.runAction(
  internal.mailingList.internal.actions.bulkSyncContactsToResend,
  {
    audienceId: "aud_123", // Optional
  },
);

console.log(`Synced: ${results.synced}, Failed: ${results.failed}`);

Update Contact Metadata

import { api } from "@/convex/_generated/api";

// When user subscription changes
await ctx.scheduler.runAfter(
  0,
  internal.mailingList.internal.actions.updateResendContactMetadataAction,
  {
    email: "user@example.com",
    userId: userId,
  },
);

Monitoring & Troubleshooting

Sync Status Indicators

In the admin dashboard, each contact shows its sync status:

  • Synced: Contact successfully synced to Resend
  • Pending: Sync scheduled but not yet completed
  • Failed: Sync failed (hover for error details)

Common Issues

Contacts Not Syncing

Check:

  1. Resend sync is enabled in settings
  2. RESEND_API_KEY is set correctly in Convex environment
  3. No errors in Convex logs: npx convex logs --tail

Sync Failures

Common causes:

  • Invalid API key
  • Rate limiting (Resend has rate limits)
  • Invalid email address format
  • Network connectivity issues

Solution: Check sync error message in admin dashboard and resolve the underlying issue.

Multi-App Conflicts

Issue: Properties from one app overwriting another

Solution: Ensure each app has a unique RESEND_APP_IDENTIFIER environment variable.

Segmentation Best Practices

Create these segments in your Resend dashboard for targeted campaigns:

  1. Newsletter Subscribers

    • Filter: {app}_subscriptionType = "newsletter" AND {app}_subscriptionStatus = "active"
  2. Paid Users

    • Filter: {app}_accountStatus = "paid" AND {app}_subscriptionStatus = "active"
  3. Free Users

    • Filter: {app}_accountStatus = "free" AND {app}_subscriptionStatus = "active"
  4. Monthly Subscribers

    • Filter: {app}_billingInterval = "monthly" AND {app}_accountStatus = "paid"
  5. Yearly Subscribers

    • Filter: {app}_billingInterval = "yearly" AND {app}_accountStatus = "paid"

Email Personalization

Use contact properties in your email templates:

<p>Hi {{firstName}},</p>

<p>Thanks for being a {{tinykit_accountStatus}} member!</p>

{{#if tinykit_billingPlan}}
<p>Your {{tinykit_billingPlan}} subscription is active.</p>
{{/if}}

Unsubscribe Flow

App-Safe Unsubscribe

The integration includes an app-safe unsubscribe mechanism:

  1. User clicks unsubscribe link in email (with app identifier)
  2. Only the app-specific {app}_subscriptionStatus property is updated to unsubscribed
  3. User remains subscribed to other apps on the same Resend account

Include this in your email templates:

<a href="{{siteUrl}}/unsubscribe?email={{email}}&app={{appIdentifier}}">
  Unsubscribe from {{appName}}
</a>

Example:

https://yourapp.com/unsubscribe?email=user@example.com&app=tinykit

API Reference

Resend Audience Helper Functions

Located in convex/lib/resendAudience.ts:

syncContactToResend(contactData, properties)

Create or update a contact in Resend with app-specific properties.

Parameters:

  • contactData: Contact email, name, and audience info
  • properties: App-specific properties to sync

Returns: { success: boolean, contactId?: string, error?: string }

updateResendContactProperties(email, properties)

Update only app-specific properties for an existing contact.

Parameters:

  • email: Contact email address
  • properties: Properties to update

Returns: { success: boolean, error?: string }

updateResendContactSubscriptionStatus(email, status)

Update app-specific subscription status (active or unsubscribed).

Parameters:

  • email: Contact email address
  • status: "active" or "unsubscribed"

Returns: { success: boolean, error?: string }

Database Schema

Mailing List Table

mailingList: defineTable({
  email: v.string(),
  status: v.union(v.literal("active"), v.literal("unsubscribed")),
  subscribedAt: v.number(),
  unsubscribedAt: v.optional(v.number()),

  // Resend sync fields
  resendContactId: v.optional(v.string()),
  lastSyncedAt: v.optional(v.number()),
  syncStatus: v.optional(
    v.union(v.literal("synced"), v.literal("pending"), v.literal("failed")),
  ),
  syncError: v.optional(v.string()),
});

Mailing List Settings

mailingListSettings: defineTable({
  // ... existing fields ...

  // Resend integration
  resendAudienceSyncEnabled: v.optional(v.boolean()),
  resendAudienceId: v.optional(v.string()),
  resendAppIdentifier: v.optional(v.string()),
});

Performance Considerations

Rate Limiting

  • Resend has API rate limits (check their documentation)
  • Sync operations are queued and processed asynchronously
  • Bulk sync processes contacts in batches to avoid timeouts

Sync Timing

  • New signups: Synced immediately (within seconds)
  • Updates: Synced immediately when data changes
  • Failed syncs: Can be retried manually from admin dashboard

Security & Privacy

Data Protection

  • API keys stored securely in Convex environment variables
  • Contact data encrypted in transit (HTTPS)
  • Only necessary data synced to Resend

GDPR Compliance

  • Unsubscribe links required in all marketing emails
  • Contact data can be deleted on request
  • Audit trail of all sync operations

Support & Resources

Changelog

Version 1.0.0 (2025-01-12)

  • Initial implementation of Resend Audience API integration
  • App-specific property namespacing for multi-app support
  • Automatic contact sync on signup/update/unsubscribe
  • Bulk sync functionality
  • Admin dashboard integration
  • Sync status monitoring

On this page

Ship your startup faster. In minutes.

Get TinyKit Pro