Email Integration
Olyron CRM sends and tracks CRM email through Resend, the platform's HTTP email provider. Each tenant can bring its own Resend API key or run on the shared platform key, and every outbound message is logged, tracked for opens and clicks, and attributed back to the contact, lead, or deal it came from.
#How email works in Olyron CRM
Outbound CRM email is sent through Resend's HTTP API (https://api.resend.com/emails). There is no SMTP relay to configure — the platform talks to Resend directly over HTTPS, so setup is just an API key and a verified sending domain. A separate SendGrid client also ships in the communications module (src/lib/integrations/communications/sendgrid.ts) for template-based transactional sends, but the tenant-facing CRM send pipeline runs on Resend.
- Credentials resolve per-tenant: a tenant's own encrypted Resend key takes priority, otherwise the platform
RESEND_API_KEYis used. - Every send first writes a row to
email_messages(tenant-scoped) before the provider call, so nothing is lost even if Resend is unreachable. - The HTML body is rewritten to route links through a click tracker and to embed a 1x1 open pixel.
- Resend delivery events (opened, clicked, replied) flow back in through a signed webhook and land in
email_opens,email_clicks, andemail_replies.
sendCrmEmail() (src/lib/services/crm/emailSend.ts). It is server-only — never import it on the client, because it needs the Resend key and the Supabase service role key.#Connecting Resend
Owners and agency admins connect email under Settings at /dashboard/settings/email. You can either paste your own Resend API key (BYOK) or opt into the shared platform key. Connecting stores an encrypted envelope of the key — the raw key is never returned by any API.
- 1Create a Resend keyIn the Resend dashboard, generate an API key and verify your sending domain so mail is not sent from the shared onboarding domain.
- 2Open Email settingsGo to /dashboard/settings/email. The page loads current status from GET /api/settings/resend, including your verified Resend domains.
- 3Save your keySubmit the key (and optionally a From address). The app calls POST /api/settings/resend/connect, which verifies the key against Resend, encrypts it, and upserts your row in tenant_email_settings with use_system_key=false.
- 4Or use the platform keyTo skip BYOK, POST /api/settings/resend/use-system-key switches the tenant to the shared RESEND_API_KEY (use_system_key=true).
curl -X POST https://core.olyron.com/api/settings/resend/connect \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <session-token>" \
-d '{
"tenant_id": "00000000-0000-0000-0000-000000000000",
"apiKey": "re_xxxxxxxxxxxxxxxx",
"fromEmail": "Acme Advisors <advisors@acme.com>"
}'#Sending an email
Send a CRM email with POST /api/crm/emails/send. The plain-text body is converted to minimal HTML, tracking is injected, the message is logged to email_messages, and a matching crm_activities row of type email is recorded against the contact, lead, or deal you reference.
tostring (email)- Recipient address. Required and validated as an email.
subjectstring- Subject line, 1–500 characters. Required.
bodystring- Plain-text body, 1–50,000 characters. Converted to HTML before sending. Required.
contactIduuid (optional)- Links the message and activity to a crm_contacts record.
leadIduuid (optional)- Attributes the send to a lead for reporting; passed to Resend as a tag.
dealIduuid (optional)- Links the message and activity to a crm_deals record.
curl -X POST https://core.olyron.com/api/crm/emails/send \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <session-token>" \
-d '{
"to": "client@example.com",
"subject": "Your policy renewal",
"body": "Hi Jordan,\n\nYour renewal window opens next week.",
"contactId": "11111111-1111-1111-1111-111111111111"
}'{
"data": {
"messageId": "a1b2c3d4-...",
"providerMessageId": "resend-message-id",
"sent": true
}
}#AI email drafts
POST /api/ai/email-draft generates a draft with Claude, enriched with CRM context. If you pass a contact_id, the endpoint pulls the contact's name, recent crm_notes, and active crm_deals into the prompt so the copy references the real relationship. Drafts are saved to crm_ai_email_drafts and returned for review — generating a draft does not send anything.
promptstring- What the email should say, 1–5,000 characters. Required.
contact_iduuid (optional)- Pulls contact, notes, and deal context into the draft.
toneenum (optional)- One of professional (default), friendly, formal, casual, or urgent.
{
"id": "draft-uuid",
"subject": "Following up on your renewal",
"body_html": "<p>Hi Jordan,</p>...",
"body_text": "Hi Jordan, ...",
"tone": "professional"
}#Tracking, replies, and webhooks
Opens and clicks are captured two ways: inline (a tracking pixel and rewritten links served from your app) and via Resend delivery events posted to the webhook. Both write into the same tables so reporting stays accurate.
| Signal | Inline endpoint | Webhook event | Table |
|---|---|---|---|
| Open | /api/track/email-open/:messageId | email.opened | email_opens |
| Click | /api/track/email-click/:messageId?u=<url> | email.clicked | email_clicks |
| Reply | — | email.replied | email_replies |
| Delivery / bounce | — | email.delivered / email.bounced | (acknowledged, no row yet) |
Point Resend's webhook at POST /api/webhooks/resend. The route verifies the Svix signature using RESEND_WEBHOOK_SECRET; if that secret and the svix-id / svix-timestamp / svix-signature headers are present, an invalid signature is rejected with 401. Events are matched to a message by provider_message_id, so unknown messages are safely ignored.
#Templates and A/B winners
Sends can reference a template variant. If you pass a templateId (and no explicit variant), sendCrmEmail looks up the template's active_variant_id — the winner promoted by the A/B sweep — and uses that variant's subject automatically, so improvements roll out without changing any calling code.
- email_templates holds the parent template and its active_variant_id.
- email_template_variants holds each subject/body variant.
- email_template_ab_tests tracks running experiments.
- An hourly Vercel cron (/api/cron/email-ab-winner, secured by CRON_SECRET) re-scores running tests and promotes a winner via runAbWinnerSweep.
#Reference: env vars and related channels
Environment variables
| Variable | Purpose |
|---|---|
| RESEND_API_KEY | Platform-wide Resend key used when a tenant is on the system key. |
| RESEND_WEBHOOK_SECRET | Svix signing secret used to verify /api/webhooks/resend. |
| RESEND_FROM_TENANT / RESEND_FROM_OLYRON | Default From headers when a tenant has not set from_email. |
| NEXT_PUBLIC_APP_URL | Base URL for open/click tracking links. |
| ANTHROPIC_API_KEY | Required for AI email drafts. |
| CRON_SECRET | Bearer secret for the A/B winner cron. |
| SENDGRID_API_KEY / SENDGRID_FROM_EMAIL / SENDGRID_FROM_NAME / SENDGRID_REPLY_TO | Config for the optional SendGrid transactional client. |
| NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY | Server-side Supabase access used by the send service. |
Related channels
Email is one of several channels. WhatsApp lives under /api/whatsapp (broadcasts, templates, conversations, webhooks) and voice under /api/voice (notes, transcriptions, next-best-action). They share the same contact and activity model, so outreach across channels stays on one timeline.