Get started
Complete integration guide
From domain verification to safe development sends, production activation and verified delivery webhooks.
1. Create a project and verify a domain
Create your workspace and a development project in the dashboard. Connect a domain whose DNS you control. Publish the exact DKIM CNAME records and custom MAIL FROM MX and SPF records shown in Domains at your DNS provider, including Namecheap. MAIL FROM uses a separate raven subdomain. Avoid creating duplicate SPF records at the same hostname. Recheck verification until the dashboard reports the domain ready. DMARC is recommended but an absent policy does not block verification.
2. Create a server-side development key
Create an email:send key in API Keys. Store the one-time secret privately as RAVENRELAY_API_KEY on your server. rr_test_ keys send real mail only to exact verified recipient domains connected to the same project. Use a mailbox you own on that domain for testing. All To, CC and BCC addresses must be allowed; otherwise the entire request is rejected with test_recipient_not_allowed. Each key has 15 accepted requests per minute and 1,000 recipient sends per UTC month. Do not put keys in browser bundles, public repositories or agent prompts.
3. Send from your server
curl https://api-production-1c6a.up.railway.app/v1/emails \ -H "Authorization: Bearer $RAVENRELAY_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: welcome-user-123' \ -d '{ "from": "hello@example.com", "to": ["user@example.com"], "subject": "Welcome", "text": "Hello from RavenRelay" }'Replace example.com with your verified domain and the recipient with your own allowed mailbox. A 202 response contains id, status and test_mode. queued means accepted for asynchronous dispatch, not delivered to an inbox. suppressed means no email will be sent. Use one stable Idempotency-Key per logical send and exactly the same payload when retrying it. Keys are printable ASCII, at most 128 characters; reuse with a different payload returns idempotency_conflict. Handle HTTP failures explicitly. For rate_limit_exceeded, wait the seconds stated in the error before retrying. Do not automatically resend delivery_unknown outcomes: provider acceptance may have happened.
4. Read outcomes
curl https://api-production-1c6a.up.railway.app/v1/emails/EMAIL_ID -H "Authorization: Bearer $RAVENRELAY_API_KEY"GET /v1/emails accepts limit (1–100, default 50), cursor, status and recipient filters. Follow next_cursor until it is null. Recipient filtering matches To addresses. A send key reads emails it created; an email:read key reads its project. sent means the delivery provider accepted the message; delivered means the receiving mail server accepted it. Neither guarantees inbox placement. The read API omits bodies and BCC. Message bodies are retained for seven days.
5. Receive signed webhooks
Create a public HTTPS endpoint on port 443 in the dashboard and subscribe to the events your application needs. Store its one-time signing secret privately. Read the original request bytes before JSON middleware. Verify RavenRelay-Timestamp and RavenRelay-Signature using HMAC-SHA256 with the literal UTF-8 secret and timestamp + a dot + the raw body. Compare signatures in constant time and reject timestamps more than five minutes away from your clock. The signature format is v1=<hex>. Only then parse and process the event.
import { createHmac, timingSafeEqual } from "node:crypto";export async function verifiedEvent(request: Request, secret: string) { const timestamp = request.headers.get("RavenRelay-Timestamp") ?? ""; const signature = request.headers.get("RavenRelay-Signature") ?? ""; if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) throw new Error("Invalid timestamp"); if (!/^v1=[a-f0-9]{64}$/.test(signature)) throw new Error("Invalid signature"); const raw = Buffer.from(await request.arrayBuffer()); const expected = createHmac("sha256", secret).update(timestamp + ".").update(raw).digest(); const received = Buffer.from(signature.slice(3), "hex"); if (!timingSafeEqual(expected, received)) throw new Error("Invalid signature"); return JSON.parse(raw.toString("utf8"));}Deduplicate the verified payload id in durable storage and acknowledge with 2xx after committing it. RavenRelay-Event-Id carries the stable ID; retries keep the same ID and body but use a fresh timestamp/signature. Events can arrive out of order. Non-2xx, timeouts and network failures retry after 1 minute, 5 minutes, 15 minutes, 1 hour, 6 hours and 24 hours: seven attempts total. Redirects are not followed. Development deliveries also generate external webhooks with data.test_mode=true. Legacy simulated messages do not. Manual replay is not currently available.
6. Respect suppressions
Hard bounces and spam complaints automatically block the recipient across the workspace. Owners and administrators can also add manual blocks. Any blocked To, CC or BCC recipient prevents the whole request from sending. Test the flow using bounce@simulator.amazonses.com and complaint@simulator.amazonses.com from an activated production project; these addresses are outside your development allow list. Send once, wait for feedback, then resend to see suppressed. Remove a real complaint block only after resolving the cause and confirming permission to contact the recipient.
7. Activate production
Request workspace activation and complete the administrator review. Create a production project, connect and verify its sender domains, then create a rr_live_ key. Switch your server configuration to that key and project. Production recipient domains are unrestricted, subject to workspace activation, suppressions and provider rules. Keep development and production secrets separate.
8. Budget and handle errors
The free allowance is 5,000 accepted recipient sends per workspace per UTC month across all projects and domains. To, CC and BCC count; real development deliveries also count. Unlimited domains and no added branding are included. There is no RavenRelay daily quota, but request rate limits and delivery provider limits apply. See project usage in Usage and aggregate usage through View workspace usage on the Usage page.
| Failure | Action |
|---|---|
401 invalid_api_key | Check server configuration and key revocation. |
403 forbidden / production_not_enabled | Check permissions and workspace activation. |
422 domain_not_verified / test_recipient_not_allowed | Verify the exact project sender and allowed development recipient domains. |
409 idempotency_conflict | Keep the payload unchanged or create a key for a genuinely new send. |
429 rate_limit_exceeded | Wait the number of seconds in the message and retry with the same idempotency key. |
429 quota_exceeded | Check the key and workspace monthly allowances; wait for the UTC reset or contact support. |
413 message_too_large | Keep the complete JSON request within 10 MiB. More than 50 total recipients returns validation_error. |