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:
| 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 Pro 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
Product Management & Sync System Guide
Complete guide to TinyKit Pro's intelligent product management system that synchronizes products between your seed configuration, Stripe account, and Convex ...
Convex Node.js Setup
Convex local deployments require Node.js v18 to be installed for running "use node" actions, even though our application runs on Node v22.