Resend Audience API Integration
TinyKit SaaS 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:
| Property | Description | Example Values |
|---|---|---|
{app}_subscriptionType | Type of subscription | newsletter, waitlist |
{app}_subscriptionStatus | Current subscription status | active, unsubscribed |
{app}_subscribedAt | Subscription timestamp | 2025-01-15T10:30:00Z |
{app}_userId | Convex user ID (if authenticated) | j57abc123... |
{app}_accountStatus | User account status | free, trial, paid |
{app}_userRole | User role in your app | user, admin |
{app}_billingPlan | Subscription plan name | Professional, Enterprise |
{app}_billingInterval | Billing cycle | monthly, yearly |
{app}_lastActivityAt | Last activity timestamp | 2025-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
- Navigate to Admin → Mailing List Settings
- Toggle Enable Resend Sync to ON
- (Optional) Enter your Resend Audience ID if you're using audiences
- Save settings
3. Initial Data Migration (Optional)
If you have existing contacts in your database, perform a one-time bulk sync:
- Go to Admin → Mailing List Settings
- Click Bulk Sync Contacts
- Wait for the sync to complete
- Check sync status in the contacts table
Usage
Automatic Sync Events
Contacts are automatically synced to Resend when:
- New Signup: User signs up for newsletter/waitlist
- Reactivation: Previously unsubscribed user re-subscribes
- Unsubscribe: User unsubscribes (updates app-specific property only)
- 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:
- Resend sync is enabled in settings
RESEND_API_KEYis set correctly in Convex environment- 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
Recommended Segments in Resend
Create these segments in your Resend dashboard for targeted campaigns:
-
Newsletter Subscribers
- Filter:
{app}_subscriptionType = "newsletter"AND{app}_subscriptionStatus = "active"
- Filter:
-
Paid Users
- Filter:
{app}_accountStatus = "paid"AND{app}_subscriptionStatus = "active"
- Filter:
-
Free Users
- Filter:
{app}_accountStatus = "free"AND{app}_subscriptionStatus = "active"
- Filter:
-
Monthly Subscribers
- Filter:
{app}_billingInterval = "monthly"AND{app}_accountStatus = "paid"
- Filter:
-
Yearly Subscribers
- Filter:
{app}_billingInterval = "yearly"AND{app}_accountStatus = "paid"
- Filter:
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:
- User clicks unsubscribe link in email (with app identifier)
- Only the app-specific
{app}_subscriptionStatusproperty is updated tounsubscribed - User remains subscribed to other apps on the same Resend account
Unsubscribe Link Format
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=tinykitAPI 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 infoproperties: 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 addressproperties: Properties to update
Returns: { success: boolean, error?: string }
updateResendContactSubscriptionStatus(email, status)
Update app-specific subscription status (active or unsubscribed).
Parameters:
email: Contact email addressstatus:"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
- Resend Documentation: https://resend.com/docs
- Resend Audience API: https://resend.com/docs/dashboard/audiences/introduction
- Resend Support: https://resend.com/support
- TinyKit SaaS Docs: See
/docsdirectory
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