diff --git a/apps/docs/api-reference/introduction.mdx b/apps/docs/api-reference/introduction.mdx index cbc4a1b..d1a4077 100644 --- a/apps/docs/api-reference/introduction.mdx +++ b/apps/docs/api-reference/introduction.mdx @@ -1,35 +1,141 @@ --- -title: Introduction -description: "Fundamental concepts of ByteSend's API." +title: API Reference +description: "Fundamental concepts of ByteSend's REST API" --- ## Base URL -ByteSend's API is built on REST principles and is served over HTTPS. To ensure data privacy, unencrypted HTTP is not supported. +ByteSend's API is served over HTTPS. All API endpoints are prefixed with: -The Base URL for all API endpoints is: - -```sh Terminal -https://bytesend.cloud/api/ ``` +https://bytesend.cloud/api/v1 +``` + + +If you're running a self-hosted ByteSend instance, replace `https://bytesend.cloud` with your instance's base URL. + + +--- ## Authentication -Authentication to ByteSend's API is performed via the Authorization header with a Bearer token. To authenticate, you need to include the Authorization header with the word Bearer followed by your token in your API requests like so: +All API endpoints (except `/v1/smtp/auth`) require a Bearer token in the `Authorization` header: -```sh Terminal -Authorization: Bearer bs_12345 +``` +Authorization: Bearer bs_your_api_key ``` -You can create a new token/API key under your ByteSend [Settings > API Keys](https://bytesend.cloud/settings/api-keys). - -### SMTP Authentication +You can create API keys under [Settings → API Keys](https://bytesend.cloud/settings/api-keys). See the [API Authentication guide](/guides/api-authentication) for full details on generating and managing keys. -The SMTP relay server uses a different authentication method. Instead of Bearer tokens, SMTP clients authenticate using: +### SMTP authentication -- **Username**: The team's custom SMTP username (or the default `bytesend`) +The SMTP relay server uses a different scheme. SMTP clients authenticate with: +- **Username**: Your team's custom SMTP username (default: `bytesend`) - **Password**: Your API key -The SMTP relay calls the [SMTP Auth endpoint](/api-reference/smtp/auth) to validate these credentials. This endpoint is **public** and does not require Bearer token authentication. +--- + +## Request Format + +All write requests (`POST`, `PATCH`, `PUT`) expect a JSON body: + +``` +Content-Type: application/json +``` + +--- + +## Response Format + +Successful responses return JSON with a `2xx` status code. The shape varies by endpoint. + +```json +{ + "id": "email_abc123", + "status": "QUEUED", + "createdAt": "2026-06-24T10:00:00.000Z" +} +``` + +--- + +## Error Format + +All errors return a consistent JSON structure: + +```json +{ + "error": { + "code": "NOT_FOUND", + "message": "Email not found" + } +} +``` + +| Field | Description | +| ----- | ----------- | +| `error.code` | Machine-readable error code string | +| `error.message` | Human-readable explanation | + +| Code | HTTP | Meaning | +| ---- | ---- | ------- | +| `BAD_REQUEST` | 400 | Invalid or missing request parameters | +| `UNAUTHORIZED` | 401 | Missing or invalid API key | +| `FORBIDDEN` | 403 | Request not permitted for this team | +| `NOT_FOUND` | 404 | Resource not found | +| `METHOD_NOT_ALLOWED` | 405 | Wrong HTTP method | +| `NOT_UNIQUE` | 409 | Resource already exists | +| `RATE_LIMITED` | 429 | Rate limit exceeded | +| `INTERNAL_SERVER_ERROR` | 500 | Unexpected server error | + +See the [Error Codes guide](/guides/error-codes) for detailed descriptions and handling examples. + +--- + +## Rate Limiting + +API requests are rate-limited per team per second. Rate limit headers are included on every response: + +| Header | Description | +| ------ | ----------- | +| `X-RateLimit-Limit` | Max requests per second | +| `X-RateLimit-Remaining` | Requests remaining in current window | +| `X-RateLimit-Reset` | Unix timestamp when window resets | +| `Retry-After` | Seconds to wait (only on `429` responses) | + +When you exceed the limit, you receive a `429 RATE_LIMITED` response. See the [Rate Limits guide](/guides/rate-limits) for retry strategies. + + +Rate limiting is disabled on self-hosted ByteSend instances. + + +--- + +## Pagination + +List endpoints return paginated results. Use the `page` and `limit` query parameters: + +``` +GET /v1/emails?page=2&limit=25 +``` + +Responses include pagination metadata: + +```json +{ + "data": [...], + "pagination": { + "page": 2, + "limit": 25, + "total": 150, + "hasNextPage": true, + "hasPrevPage": true + } +} +``` + +--- + +## Interactive Playground -For more details, see the [SMTP relay documentation](/self-hosting/smtp-server). +Every endpoint in the API Reference has a built-in request playground. Click the **Try it** button on any endpoint page to send live requests directly from the docs using your API key. diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 3daccda..3168377 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -64,6 +64,8 @@ "group": "Account & Authentication", "pages": [ "guides/github-oauth", + "guides/google-oauth", + "guides/discord-oauth", "guides/api-authentication" ] }, @@ -72,7 +74,24 @@ "pages": [ "guides/plans-and-pricing", "guides/plan-management", - "guides/admin-operations" + "guides/admin-operations", + "guides/rate-limits" + ] + }, + { + "group": "Sending Email", + "pages": [ + "guides/contact-books", + "guides/campaigns", + "guides/double-opt-in", + "guides/campaign-personalization", + "guides/use-with-react-email" + ] + }, + { + "group": "Deliverability", + "pages": [ + "guides/deliverability" ] }, { @@ -87,9 +106,7 @@ "group": "Integration Guides", "pages": [ "guides/webhooks", - "guides/double-opt-in", - "guides/campaign-personalization", - "guides/use-with-react-email" + "guides/error-codes" ] } ] diff --git a/apps/docs/get-started/nodejs.mdx b/apps/docs/get-started/nodejs.mdx index 2f42e45..b9c8d81 100644 --- a/apps/docs/get-started/nodejs.mdx +++ b/apps/docs/get-started/nodejs.mdx @@ -1,97 +1,277 @@ --- -title: NodeJS -description: "Send your mail using ByteSend in NodeJS" +title: Node.js +description: "Send emails from Node.js and TypeScript using the ByteSend SDK" icon: node-js --- ## Prerequisites -- [ByteSend API key](https://bytesend.cloud/settings/api-keys) -- [Verified domain](https://bytesend.cloud/domains) - -## Using SDK - - - - - ```bash npm - npm install bytesend-js - ``` - - ```bash yarn - yarn add bytesend-js - ``` - - ```bash pnpm - pnpm add bytesend-js - ``` - - ```bash bun - bun add bytesend-js - ``` - - - - - Get the API key from the [ByteSend dashboard](https://bytesend.cloud/settings/api-keys) and initialize the SDK - - ```javascript - import { ByteSend } from "bytesend-js"; - - const bytesend = new ByteSend("bs_12345"); - ``` - - If you are running a self-hosted version of ByteSend, pass the base URL as the - second argument: - - ```javascript - const bytesend = new ByteSend("bs_12345", "https://your-bytesend-instance.com"); - ``` - - - - ```javascript - bytesend.emails.send({ - to: "hello@acme.com", - from: "hello@company.com", - subject: "ByteSend email", - html: "

ByteSend is the best way to send emails

", - text: "ByteSend is the best way to send emails", - headers: { - "X-Campaign": "welcome", - }, - }); - ``` - - > Custom headers are forwarded as-is. ByteSend only manages the `X-ByteSend-Email-ID` and `References` headers. -
-
- -## Adding contacts programatically - - - - Get the contact book id from the [ByteSend dashboard](https://bytesend.cloud/contacts/). Copy the contact book id - - - - ```javascript - bytesend.contacts - .create("clzeydgeygff", { - email: "hey@bobdole.com", - firstName: "Bob", - lastName: "Dole", - }) - ``` - - - - - ```javascript - bytesend.contacts.update("clzeydgeygff", contactId, { - firstName: "Bob", - lastName: "Dole", - }); - ``` - - +- Node.js 18 or later +- A [ByteSend API key](https://bytesend.cloud/settings/api-keys) +- A [verified domain](https://bytesend.cloud/domains) to send from + +--- + +## Installation + + +```bash npm +npm install bytesend-js +``` + +```bash yarn +yarn add bytesend-js +``` + +```bash pnpm +pnpm add bytesend-js +``` + +```bash bun +bun add bytesend-js +``` + + +--- + +## Quick Start + +```typescript +import { ByteSend } from "bytesend-js"; + +const bytesend = new ByteSend(process.env.BYTESEND_API_KEY!); + +const email = await bytesend.emails.send({ + from: "hello@yourdomain.com", + to: "recipient@example.com", + subject: "Hello from ByteSend", + html: "

This email was sent via the ByteSend SDK.

", + text: "This email was sent via the ByteSend SDK.", +}); + +console.log("Sent:", email.id); +``` + +Store your API key in an environment variable, not in version control: + +```bash .env +BYTESEND_API_KEY=bs_your_api_key_here +``` + +--- + +## Self-Hosted Instances + +If you're running a self-hosted ByteSend, pass your instance URL as the second argument: + +```typescript +const bytesend = new ByteSend( + process.env.BYTESEND_API_KEY!, + "https://your-bytesend-instance.com" +); +``` + +--- + +## Sending Emails + +### Basic send + +```typescript +await bytesend.emails.send({ + from: "team@yourdomain.com", + to: "user@example.com", + subject: "Welcome!", + html: "

Welcome to our app

", +}); +``` + +### Multiple recipients + +```typescript +await bytesend.emails.send({ + from: "team@yourdomain.com", + to: ["alice@example.com", "bob@example.com"], + subject: "Team announcement", + html: "

Something big is happening.

", +}); +``` + +### With CC, BCC, and Reply-To + +```typescript +await bytesend.emails.send({ + from: "support@yourdomain.com", + to: "customer@example.com", + cc: "manager@yourdomain.com", + bcc: "archive@yourdomain.com", + replyTo: "noreply@yourdomain.com", + subject: "Your support ticket", + html: "

We've received your request.

", +}); +``` + +### Scheduling an email + +```typescript +await bytesend.emails.send({ + from: "team@yourdomain.com", + to: "user@example.com", + subject: "Scheduled message", + html: "

This was scheduled ahead of time.

", + scheduledAt: new Date("2026-07-01T09:00:00Z").toISOString(), +}); +``` + +### Custom headers and metadata + +```typescript +await bytesend.emails.send({ + from: "team@yourdomain.com", + to: "user@example.com", + subject: "Order confirmation", + html: "

Your order #12345 has been confirmed.

", + headers: { + "X-Order-ID": "12345", + "X-Customer-Tier": "premium", + }, + metadata: { + orderId: "12345", + userId: "usr_abc", + }, +}); +``` + +--- + +## Batch Sending + +Send up to 100 emails in a single API call. Much more efficient than looping individual requests: + +```typescript +await bytesend.emails.sendBatch([ + { + from: "team@yourdomain.com", + to: "alice@example.com", + subject: "Your invoice", + html: "

Invoice for Alice

", + }, + { + from: "team@yourdomain.com", + to: "bob@example.com", + subject: "Your invoice", + html: "

Invoice for Bob

", + }, +]); +``` + +--- + +## Error Handling + +The SDK throws errors for failed API calls. Always wrap sends in try/catch in production: + +```typescript +import { ByteSend } from "bytesend-js"; + +const bytesend = new ByteSend(process.env.BYTESEND_API_KEY!); + +try { + const email = await bytesend.emails.send({ + from: "team@yourdomain.com", + to: "user@example.com", + subject: "Hello", + html: "

Hello!

", + }); + console.log("Email queued:", email.id); +} catch (error) { + if (error instanceof Error) { + console.error("Send failed:", error.message); + } +} +``` + +See the [Error Codes](/guides/error-codes) guide for the full list of API error codes and how to handle each one. + +--- + +## Working with Contacts + +### Add a contact to a contact book + +```typescript +const contact = await bytesend.contacts.create("your_contact_book_id", { + email: "jane@example.com", + firstName: "Jane", + lastName: "Smith", + properties: { + plan: "pro", + signupSource: "landing-page", + }, +}); +``` + +### Upsert (create or update) + +```typescript +await bytesend.contacts.upsert("your_contact_book_id", { + email: "jane@example.com", + firstName: "Jane", + properties: { plan: "enterprise" }, +}); +``` + +### Unsubscribe a contact + +```typescript +await bytesend.contacts.update("contact_book_id", "contact_id", { + subscribed: false, +}); +``` + +--- + +## Webhook Verification + +Verify incoming webhook signatures to ensure requests are from ByteSend: + +```typescript +import { ByteSend } from "bytesend-js"; + +const bytesend = new ByteSend(process.env.BYTESEND_API_KEY!); +const webhooks = bytesend.webhooks(process.env.BYTESEND_WEBHOOK_SECRET!); + +// Next.js App Router example +export async function POST(request: Request) { + const rawBody = await request.text(); + + const event = webhooks.constructEvent(rawBody, { + headers: request.headers, + }); + + switch (event.type) { + case "email.delivered": + console.log("Delivered to:", event.data.to); + break; + case "email.bounced": + console.log("Bounced:", event.data.id, event.data.bounce?.type); + break; + case "email.complained": + console.log("Complaint from:", event.data.to); + break; + } + + return new Response("ok"); +} +``` + +See the full [Webhooks guide](/guides/webhooks) for all event types and best practices. + +--- + +## Next Steps + +- [Send your first email via the API](/api-reference/emails/send-email) +- [Set up webhooks](/guides/webhooks) +- [Work with contact books](/guides/contact-books) +- [View the full API reference](/api-reference/introduction) diff --git a/apps/docs/guides/api-authentication.mdx b/apps/docs/guides/api-authentication.mdx index a12a59b..cc2b445 100644 --- a/apps/docs/guides/api-authentication.mdx +++ b/apps/docs/guides/api-authentication.mdx @@ -37,14 +37,12 @@ curl -X GET https://bytesend.cloud/api/v1/domains \ #### JavaScript/TypeScript ```typescript -import { ByteSend } from '@bytesend/js'; +import { ByteSend } from "bytesend-js"; -const client = new ByteSend({ - apiKey: process.env.BYTESEND_API_KEY, -}); +const bytesend = new ByteSend(process.env.BYTESEND_API_KEY!); // Now use the client -const emails = await client.emails.list(); +const emails = await bytesend.emails.list(); ``` #### Python @@ -52,10 +50,10 @@ const emails = await client.emails.list(); ```python from bytesend import ByteSend -client = ByteSend(api_key=os.getenv('BYTESEND_API_KEY')) +bytesend = ByteSend(api_key=os.getenv("BYTESEND_API_KEY")) # Now use the client -emails = client.emails.list() +emails = bytesend.emails.list() ``` ## API Key Management diff --git a/apps/docs/guides/campaigns.mdx b/apps/docs/guides/campaigns.mdx new file mode 100644 index 0000000..eaa0352 --- /dev/null +++ b/apps/docs/guides/campaigns.mdx @@ -0,0 +1,200 @@ +--- +title: Campaigns & Broadcasts +description: "Send marketing emails and broadcast messages to your contact lists" +icon: megaphone +--- + +## What Is a Campaign? + +A campaign is a marketing email sent to a **contact book**: one message, many recipients. Use campaigns for: + +- Newsletters and product updates +- Promotional emails and announcements +- Onboarding sequences and drip emails +- Re-engagement campaigns + +Campaigns are different from **transactional emails** (which go to a single recipient in response to an action). Campaigns target your subscriber lists and are subject to monthly send limits based on your plan. + +--- + +## Campaigns vs. Transactional Emails + +| Feature | Campaigns | Transactional | +| ------- | --------- | ------------- | +| Recipients | Contact book | Individual `to` address | +| Personalization | Contact properties (`{{firstName}}`) | Custom per-request | +| Scheduling | Yes — schedule ahead | No | +| Unsubscribe | Automatic opt-out handling | Manual | +| Use case | Marketing, newsletters | Receipts, alerts, auth codes | +| Dashboard | Campaign analytics | Email logs | + +--- + +## Creating a Campaign + +### Via the dashboard + +1. Go to [Campaigns](https://bytesend.cloud/campaigns) +2. Click **New Campaign** +3. Fill in: + - **Name** — internal label (not visible to recipients) + - **Subject** — email subject line + - **From address** — must use a [verified domain](https://bytesend.cloud/domains) + - **Reply-to** (optional) +4. Design your email in the built-in editor or paste HTML +5. Select the **contact book** to send to +6. Click **Save Draft** + +### Via the API + +```bash +curl -X POST https://bytesend.cloud/api/v1/campaigns \ + -H "Authorization: Bearer bs_your_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "June Newsletter", + "subject": "What'\''s new in June", + "from": "hello@example.com", + "contactBookId": "clzeydgeygff" + }' +``` + +--- + +## Campaign States + +| State | Description | +| ----- | ----------- | +| `DRAFT` | Saved but not sent or scheduled | +| `SCHEDULED` | Will send at the specified time | +| `SENDING` | Currently being delivered | +| `SENT` | Delivery complete | +| `PAUSED` | Manually paused mid-send | +| `CANCELLED` | Cancelled before or during send | + +--- + +## Scheduling a Campaign + +Schedule a campaign to send at a future time: + +```bash +curl -X POST https://bytesend.cloud/api/v1/campaigns/{campaignId}/schedule \ + -H "Authorization: Bearer bs_your_api_key" \ + -H "Content-Type: application/json" \ + -d '{"scheduledAt": "2026-07-01T09:00:00Z"}' +``` + +Campaigns can be re-scheduled or cancelled at any time before the send starts. + +### Cancelling a scheduled campaign + +```bash +curl -X DELETE https://bytesend.cloud/api/v1/campaigns/{campaignId} \ + -H "Authorization: Bearer bs_your_api_key" +``` + +--- + +## Pausing and Resuming + +If a campaign is in progress and you need to stop it (for example, you spotted a typo), you can pause it: + +```bash +# Pause +curl -X POST https://bytesend.cloud/api/v1/campaigns/{campaignId}/pause \ + -H "Authorization: Bearer bs_your_api_key" + +# Resume +curl -X POST https://bytesend.cloud/api/v1/campaigns/{campaignId}/resume \ + -H "Authorization: Bearer bs_your_api_key" +``` + + +Emails already sent before the pause are not recalled. Pausing stops further sends to remaining recipients. + + +--- + +## Personalization + +Use `{{variableName}}` syntax in your subject line and email body to inject contact properties: + +``` +Subject: Hi {{firstName}}, here's what's new this month +``` + +```html +

Hey {{firstName}},

+

You're on the {{plan}} plan — here's what's new for you.

+``` + +If a contact doesn't have a value for a variable, it renders as an empty string. See [Campaign Personalization](/guides/campaign-personalization) for advanced usage and fallback values. + +--- + +## Unsubscribe Handling + +ByteSend automatically handles unsubscribes: + +- Every campaign email includes an unsubscribe link in the footer (added automatically) +- When a recipient clicks unsubscribe, their contact status is set to **Unsubscribed** +- Unsubscribed contacts are excluded from all future campaign sends +- You don't need to handle this manually + +--- + +## Campaign Analytics + +After sending, view per-campaign metrics in the dashboard: + +- **Delivered** — successfully reached the inbox +- **Opened** — recipient opened the email +- **Clicked** — recipient clicked a link +- **Bounced** — permanently undeliverable addresses +- **Complained** — marked as spam +- **Unsubscribed** — opted out via the unsubscribe link + +You can also pull these via the [Analytics API](/api-reference/analytics/email-time-series). + +--- + +## Plan Limits + +| Plan | Campaigns | Monthly emails | +| ---- | --------- | -------------- | +| Free | 3 | 12,500 | +| Hobby | 30 | 25,000 | +| Lite | 100 | 50,000 | +| Pro | 200 | 100,000 | +| Lifetime | 1,000 | 500,000 | + +Marketing emails are included on all plans. Overage billing applies on paid plans when you exceed the monthly limit. + +--- + +## Best Practices + + + + Send a test email to yourself from the campaign preview screen before scheduling to a full contact book. + + + If your domain is new or you haven't sent campaigns in a while, start with a smaller segment and ramp up. See the [Deliverability guide](/guides/deliverability) for warm-up recommendations. + + + Use multiple contact books to segment by interest, plan, or engagement level. A targeted campaign to 500 engaged users will always outperform a blast to 5,000 cold contacts. + + + After each campaign, check the [Deliverability dashboard](https://bytesend.cloud/dashboard) for bounce and complaint rates. If either spikes, pause campaigns and clean your list before the next send. + + + +--- + +## Next Steps + +- [Set up Contact Books](/guides/contact-books) +- [Personalize your campaigns](/guides/campaign-personalization) +- [Monitor deliverability](/guides/deliverability) +- [API Reference: Campaigns](/api-reference/campaigns/create-campaign) diff --git a/apps/docs/guides/contact-books.mdx b/apps/docs/guides/contact-books.mdx new file mode 100644 index 0000000..ceed2a4 --- /dev/null +++ b/apps/docs/guides/contact-books.mdx @@ -0,0 +1,162 @@ +--- +title: Contact Books +description: "Organize your subscribers and contacts into manageable groups" +icon: address-book +--- + +## What Is a Contact Book? + +A contact book is a mailing list. Each contact has an email address, optional name fields, a subscription status, and any number of custom properties you define. + +Contact books are the foundation of ByteSend's marketing features. You need at least one contact book before you can: +- Send broadcast campaigns +- Use double opt-in confirmation flows +- Segment your audience for targeted sends + +--- + +## Creating a Contact Book + +### Via the dashboard + +1. Go to [Contacts](https://bytesend.cloud/contacts) +2. Click **New Contact Book** +3. Give it a name (e.g. "Newsletter Subscribers", "Product Updates") +4. Optionally enable [Double Opt-In](/guides/double-opt-in) +5. Click **Create** + +### Via the API + +```bash +curl -X POST https://bytesend.cloud/api/v1/contactBooks \ + -H "Authorization: Bearer bs_your_api_key" \ + -H "Content-Type: application/json" \ + -d '{"name": "Newsletter Subscribers"}' +``` + +```typescript +const book = await bytesend.contactBooks.create({ + name: "Newsletter Subscribers", +}); +console.log(book.id); // keep this id for adding contacts +``` + +--- + +## Adding Contacts + +Contacts can be added one at a time, in bulk, or imported from a CSV. + +### Single contact + +```typescript +await bytesend.contacts.create(contactBookId, { + email: "jane@example.com", + firstName: "Jane", + lastName: "Smith", +}); +``` + +### Bulk import (API) + +Use the bulk create endpoint to add up to 1,000 contacts in one request: + +```typescript +await bytesend.contacts.bulkCreate(contactBookId, [ + { email: "alice@example.com", firstName: "Alice" }, + { email: "bob@example.com", firstName: "Bob" }, + { email: "carol@example.com", firstName: "Carol" }, +]); +``` + +### CSV import (dashboard) + +1. Go to your contact book → **Import** +2. Upload a CSV with columns: `email`, `firstName` (optional), `lastName` (optional) +3. Map CSV columns to contact fields +4. Click **Import** + +--- + +## Contact Statuses + +Every contact in a contact book has one of these statuses: + +| Status | Description | +| ------ | ----------- | +| **Subscribed** | Active and will receive campaign emails | +| **Pending** | Added but awaiting double opt-in confirmation | +| **Unsubscribed** | Opted out — will not receive campaign emails | + + +Contacts with **Pending** or **Unsubscribed** status are excluded from campaign sends. They will not receive any marketing emails from ByteSend. + + +--- + +## Custom Properties + +Beyond the standard `email`, `firstName`, and `lastName` fields, contacts support custom key-value properties for anything relevant to your audience: plan tier, signup source, account ID, etc. + +```typescript +await bytesend.contacts.create(contactBookId, { + email: "alice@example.com", + firstName: "Alice", + properties: { + plan: "pro", + signupSource: "landing-page", + userId: "usr_abc123", + }, +}); +``` + +Custom properties can then be used in [campaign personalization](/guides/campaign-personalization) via `{{plan}}`, `{{signupSource}}`, etc. + +--- + +## Upserting Contacts + +The upsert endpoint creates a contact if they don't exist, or updates their record if they do. This is ideal for syncing contacts from your app on signup or profile update: + +```typescript +await bytesend.contacts.upsert(contactBookId, { + email: "alice@example.com", + firstName: "Alice", + properties: { plan: "pro" }, +}); +``` + +--- + +## Unsubscribing Contacts + +When a recipient unsubscribes, update their status immediately: + +```typescript +await bytesend.contacts.update(contactBookId, contactId, { + subscribed: false, +}); +``` + +If you handle unsubscribes via webhooks (e.g., reading from `email.complained`), do this automatically in your webhook handler so your list stays clean. + +--- + +## Plan Limits + +| Plan | Contact Books | Contacts per account | +| ---- | ------------- | -------------------- | +| Free | 5 | 100 | +| Hobby | 10 | 200 | +| Lite | 25 | 300 | +| Pro | 50 | 1,000 | +| Lifetime | 200 | 10,000 | + +--- + +## Next Steps + +- [Enable Double Opt-In](/guides/double-opt-in) to verify new subscribers +- [Send a Campaign](/guides/campaigns) to your contact book +- [Personalize emails](/guides/campaign-personalization) with contact properties +- [API Reference: Contact Books](/api-reference/contacts/list-contact-books) diff --git a/apps/docs/guides/deliverability.mdx b/apps/docs/guides/deliverability.mdx new file mode 100644 index 0000000..f07b788 --- /dev/null +++ b/apps/docs/guides/deliverability.mdx @@ -0,0 +1,198 @@ +--- +title: Deliverability & Reputation +description: "Keep your bounce and complaint rates healthy to maintain email deliverability" +icon: shield-check +--- + +## Overview + +Email deliverability is the ability to land in your recipients' inboxes rather than their spam folders. ByteSend tracks your sending reputation in real time and automatically pauses sending when your bounce or complaint rates get too high. + +--- + +## Reputation Metrics + +ByteSend calculates two key reputation metrics across a **7-day rolling window**: + +### Hard Bounce Rate + +A hard bounce means the email was permanently undeliverable. The address doesn't exist or the domain is invalid. + +| Threshold | Action | +| --------- | ------ | +| Below **1.5%** | Healthy — no action | +| **1.5% – 2%** | Warning displayed in dashboard | +| **2% or above** | Sending automatically blocked | + +### Complaint Rate + +A complaint is recorded when a recipient marks your email as spam. + +| Threshold | Action | +| --------- | ------ | +| Below **0.08%** | Healthy — no action | +| **0.08% – 0.1%** | Warning displayed in dashboard | +| **0.1% or above** | Sending automatically blocked | + + +When your sending is blocked due to reputation thresholds, all outgoing emails (transactional and campaign) are paused until your rates recover or you contact support. API keys remain active but sends will be rejected. + + +You can view your current metrics on the [Dashboard](https://bytesend.cloud/dashboard) under **Reputation Metrics**. + +--- + +## Why These Thresholds? + +ByteSend's thresholds align with major inbox providers' requirements: + +- **Google Gmail** recommends keeping complaint rates below 0.1% and will begin filtering at 0.3% +- **Microsoft Outlook** enforces strict bounce limits and may block senders above 2% +- **AWS SES** (which ByteSend uses for cloud delivery) has similar thresholds + +Exceeding these limits doesn't just block your account. It affects deliverability for everyone on the platform. + +--- + +## Improving Your Reputation + +### 1. Clean Your List Regularly + +Remove addresses that have bounced, unsubscribed, or haven't engaged in 90+ days. A smaller, engaged list always outperforms a large stale one. + + + + Export your contact book from the [Contacts dashboard](https://bytesend.cloud/contacts) or via the API. + + + Remove any addresses that appeared in `email.bounced` webhook events with `bounce.type: "Permanent"`. + + + Contacts who haven't opened an email in 90 days are at high risk of marking future emails as spam. + + + Enable [Double Opt-In](/guides/double-opt-in) on your contact books to ensure only confirmed, valid addresses are added. + + + +### 2. Set Up SPF, DKIM, and DMARC + +ByteSend handles DKIM signing automatically when you [verify your domain](https://bytesend.cloud/domains). Make sure you also configure: + +- **SPF** — Add ByteSend to your domain's allowed senders +- **DMARC** — A DMARC record tells inbox providers what to do if authentication fails. Start with `p=none` to monitor, then move to `p=quarantine` once you've confirmed alignment. + +All three are shown in the domain verification flow in the dashboard. + +### 3. Warm Up New Domains + +Never start sending full volume from a brand-new domain. Inbox providers are suspicious of new domains sending thousands of emails immediately. + +**Recommended warm-up schedule:** + +| Day | Max emails/day | +| --- | -------------- | +| 1–3 | 50–100 | +| 4–7 | 250–500 | +| 8–14 | 1,000–2,500 | +| 15–21 | 5,000–10,000 | +| 22+ | Normal volume | + +Start with your most engaged users: people who have recently interacted with your product. + +### 4. Write Content That Doesn't Trigger Spam Filters + +- Use a clear `from` name and a recognizable sender domain +- Include a plain-text version alongside HTML emails +- Avoid spam trigger words ("FREE!!!", excessive capitalization, deceptive subjects) +- Keep your image-to-text ratio reasonable. Pure-image emails perform poorly. +- Always include a visible, working unsubscribe link + +### 5. Monitor Bounces and Complaints in Real Time + +Set up a [webhook](/guides/webhooks) for `email.bounced` and `email.complained` events so you can react immediately: + +```typescript +import { ByteSend } from "bytesend-js"; + +const bytesend = new ByteSend(process.env.BYTESEND_API_KEY!); +const webhooks = bytesend.webhooks(process.env.BYTESEND_WEBHOOK_SECRET!); + +export async function POST(request: Request) { + const rawBody = await request.text(); + const event = webhooks.constructEvent(rawBody, { headers: request.headers }); + + switch (event.type) { + case "email.bounced": + if (event.data.bounce?.type === "Permanent") { + // Remove from your contact book or mark as unsubscribed + await bytesend.contacts.update(contactBookId, event.data.contactId!, { + subscribed: false, + }); + } + break; + + case "email.complained": + // Immediately unsubscribe anyone who complains + await bytesend.contacts.update(contactBookId, event.data.contactId!, { + subscribed: false, + }); + break; + } + + return new Response("ok"); +} +``` + +### 6. Set Up Notification Alerts + +Configure [Discord or Slack notifications](/guides/notification-providers) for `EMAIL_BOUNCED` and `EMAIL_COMPLAINED` events. You'll get real-time alerts when your bounce or complaint rates spike, giving you time to pause campaigns before hitting the automatic block threshold. + +--- + +## Suppression Lists + +ByteSend maintains an automatic suppression list. Addresses that hard bounce are added automatically and will be silently skipped on future sends rather than bouncing again, protecting your reputation. + +If you need to manually remove an address from the suppression list (e.g., after the recipient fixed a typo in their email), contact support via [Discord](https://discord.gg/Bg3Sf5fqa4). + +--- + +## Recovering From a Block + +If your sending is blocked due to reputation thresholds: + +1. **Don't panic.** The block is temporary and reversible. +2. **Check the dashboard** for the specific metric that triggered it. +3. **Clean your list.** Remove bounced addresses and unsubscribe anyone who has complained. +4. **Wait for the 7-day window to roll.** As old events age out, your rate will naturally improve. +5. **Contact support** on [Discord](https://discord.gg/Bg3Sf5fqa4) if you need the block lifted sooner and can explain why the spike occurred + +--- + +## Reputation Checklist + + + + - SPF record added and correct + - DKIM verified in ByteSend dashboard + - DMARC policy configured (start with `p=none`) + + + - Double opt-in enabled on contact books + - Bounced addresses removed within 24 hours + - Complaint-ers immediately unsubscribed + - List cleaned of inactive contacts every 90 days + + + - New domains warmed up gradually + - Not sending to purchased or scraped lists + - Unsubscribe link visible and functional + - Plain-text alternative included + + + - Webhooks configured for `email.bounced` and `email.complained` + - Notification alerts enabled + - Dashboard reputation metrics reviewed weekly + + diff --git a/apps/docs/guides/discord-oauth.mdx b/apps/docs/guides/discord-oauth.mdx new file mode 100644 index 0000000..1fc83d4 --- /dev/null +++ b/apps/docs/guides/discord-oauth.mdx @@ -0,0 +1,86 @@ +--- +title: Discord OAuth Authentication +description: "Sign in to ByteSend using your Discord account" +icon: discord +--- + +## Overview + +ByteSend supports Discord OAuth, letting you sign in with your Discord account for quick, passwordless access. + +## Benefits + +- **Single Sign-On**: Use your Discord account to access ByteSend +- **Automatic avatar sync**: Your Discord profile picture is imported and kept up to date on each sign-in +- **Community integration**: Same account you use in our [Discord server](https://discord.gg/Bg3Sf5fqa4) +- **Account linking**: Connect Discord to an existing ByteSend account + +## Signing In with Discord + +1. Visit [ByteSend](https://bytesend.cloud) +2. Click **Sign in with Discord** on the login page +3. Log in to Discord if prompted, then click **Authorize** +4. You'll be redirected back to the ByteSend dashboard + +### First-time sign-in + +On your first Discord login, ByteSend will: +- Create a new account linked to your Discord email +- Import your Discord avatar as your profile picture +- Set up a default workspace for your account + +### Linking to an existing account + +If you already have a ByteSend account using the same email address as your Discord account, OAuth will automatically link them. You can then sign in with either method going forward. + +## Permissions Requested + +ByteSend requests the minimal Discord scopes: + +- **`identify`**: Your Discord username and avatar +- **`email`**: Your Discord account email address + +ByteSend never requests access to your Discord servers, messages, or any guild data. + +## Profile Picture Sync + +Your Discord avatar is refreshed automatically on every sign-in. If you update your Discord avatar, sign out and back in to ByteSend to sync the latest version. + +## Security + +- OAuth tokens are encrypted at rest and never exposed in responses or logs +- Tokens are refreshed automatically on each session +- Signing out of ByteSend does not affect your Discord session + +## Revoking Access + +To disconnect ByteSend from your Discord account: + +1. Open Discord and go to **User Settings → Authorized Apps** +2. Find "ByteSend" and click **Deauthorize** +3. You'll no longer be able to sign in with Discord until you re-authorize + +After revoking, you can still sign in via email magic link if your account uses the same email as your Discord account. + +## Troubleshooting + + + + Discord OAuth must be enabled by the platform. On self-hosted ByteSend, the administrator needs to set `DISCORD_CLIENT_ID` and `DISCORD_CLIENT_SECRET`. Contact your instance admin. + + + Your avatar is synced from Discord on each sign-in. Sign out and back in to force a refresh. + + + Clear your browser cookies and try again. If you're using a VPN, try disabling it. Discord sometimes blocks OAuth callbacks from certain regions. + + + Contact [ByteSend Support on Discord](https://discord.gg/Bg3Sf5fqa4) and include both email addresses. We can manually unlink accounts. + + + +## Next Steps + +- [Create your first API key](/guides/api-authentication) +- [Add a verified domain](https://bytesend.cloud/domains) +- [Join the ByteSend Discord community](https://discord.gg/Bg3Sf5fqa4) diff --git a/apps/docs/guides/error-codes.mdx b/apps/docs/guides/error-codes.mdx new file mode 100644 index 0000000..6897f5f --- /dev/null +++ b/apps/docs/guides/error-codes.mdx @@ -0,0 +1,281 @@ +--- +title: Error Codes +description: "Understand and handle API errors returned by ByteSend" +icon: triangle-exclamation +--- + +## Error Response Format + +All API errors return a consistent JSON structure with an HTTP status code: + +```json +{ + "error": { + "code": "NOT_FOUND", + "message": "Email not found" + } +} +``` + +| Field | Description | +| ----------------- | ------------------------------------------------ | +| `error.code` | Machine-readable error code string | +| `error.message` | Human-readable description of what went wrong | + +--- + +## Error Codes + +### `BAD_REQUEST` (400) + +The request body or parameters are invalid or missing required fields. + +```json +{ + "error": { + "code": "BAD_REQUEST", + "message": "Invalid email address in 'to' field" + } +} +``` + +**Common causes:** +- Missing required fields (`to`, `from`, `subject`) +- Invalid email address format +- Invalid enum value (e.g., a campaign status that doesn't exist) +- Malformed JSON body + +--- + +### `UNAUTHORIZED` (401) + +No API key was provided, or the provided key is invalid. + +```json +{ + "error": { + "code": "UNAUTHORIZED", + "message": "Missing or invalid API key" + } +} +``` + +**Common causes:** +- Missing `Authorization` header +- API key has been revoked +- Typo in the API key value + +**Fix:** Include a valid Bearer token in every request: +``` +Authorization: Bearer bs_your_api_key +``` + +--- + +### `FORBIDDEN` (403) + +The API key is valid but the action is not permitted. This usually happens because the team is blocked, the resource belongs to another team, or the plan doesn't allow it. + +```json +{ + "error": { + "code": "FORBIDDEN", + "message": "This team has been blocked. Please contact support via Discord: https://discord.gg/Bg3Sf5fqa4" + } +} +``` + +**Common causes:** +- Team is blocked by platform administrators +- Attempting to access a resource that belongs to another team +- Action not permitted on the current plan + +--- + +### `NOT_FOUND` (404) + +The requested resource does not exist, or it exists but belongs to a different team. + +```json +{ + "error": { + "code": "NOT_FOUND", + "message": "Email not found" + } +} +``` + +**Common causes:** +- Invalid or deleted email, contact, domain, or campaign ID +- ID belongs to a different team's resource + +--- + +### `METHOD_NOT_ALLOWED` (405) + +The HTTP method used is not supported by this endpoint. + +```json +{ + "error": { + "code": "METHOD_NOT_ALLOWED", + "message": "Method not allowed" + } +} +``` + +**Fix:** Check the API reference for the correct HTTP method (GET, POST, PATCH, DELETE, etc.). + +--- + +### `NOT_UNIQUE` (409) + +A resource with the same unique identifier already exists. + +```json +{ + "error": { + "code": "NOT_UNIQUE", + "message": "A domain with this name already exists" + } +} +``` + +**Common causes:** +- Adding a domain that's already registered to your team +- Creating a contact with an email address that already exists in that contact book + +--- + +### `RATE_LIMITED` (429) + +You have exceeded the API rate limit. See [Rate Limits](/guides/rate-limits) for details. + +```json +{ + "error": { + "code": "RATE_LIMITED", + "message": "Rate limit exceeded. Try again in 1 seconds." + } +} +``` + +The response also includes a `Retry-After` header indicating how many seconds to wait before retrying. + +--- + +### `INTERNAL_SERVER_ERROR` (500) + +An unexpected error occurred on the ByteSend server. These are rare and typically transient. + +```json +{ + "error": { + "code": "INTERNAL_SERVER_ERROR", + "message": "something unexpected happened" + } +} +``` + +**What to do:** +- Retry the request with exponential backoff +- If the error persists, contact support via [Discord](https://discord.gg/Bg3Sf5fqa4) + +--- + +## Handling Errors in Code + +### JavaScript / TypeScript + +```typescript +import { ByteSend } from "bytesend-js"; + +const bytesend = new ByteSend(process.env.BYTESEND_API_KEY!); + +try { + const email = await bytesend.emails.send({ + from: "hello@example.com", + to: "recipient@example.com", + subject: "Hello", + html: "

Hello!

", + }); + console.log("Sent:", email.id); +} catch (error) { + if (error instanceof Error) { + console.error("Failed:", error.message); + } +} +``` + +### Handling specific codes (raw fetch) + +```typescript +const response = await fetch("https://bytesend.cloud/api/v1/emails", { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.BYTESEND_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ from: "...", to: "...", subject: "...", html: "..." }), +}); + +if (!response.ok) { + const { error } = await response.json(); + + switch (error.code) { + case "UNAUTHORIZED": + console.error("Check your API key"); + break; + case "RATE_LIMITED": + const retryAfter = response.headers.get("Retry-After"); + console.error(`Rate limited. Retry in ${retryAfter}s`); + break; + case "BAD_REQUEST": + console.error("Invalid request:", error.message); + break; + default: + console.error(`API error ${error.code}: ${error.message}`); + } +} +``` + +### Python + +```python +import bytesend +import time + +client = bytesend.ByteSend(api_key=os.environ["BYTESEND_API_KEY"]) + +try: + email = client.emails.send( + from_="hello@example.com", + to="recipient@example.com", + subject="Hello", + html="

Hello!

", + ) + print(f"Sent: {email.id}") +except bytesend.APIError as e: + if e.code == "RATE_LIMITED": + retry_after = e.headers.get("Retry-After", 1) + time.sleep(int(retry_after)) + elif e.code == "UNAUTHORIZED": + print("Invalid API key") + else: + print(f"Error {e.code}: {e.message}") +``` + +--- + +## Summary Table + +| Code | HTTP | When it happens | +| ---------------------- | ---- | -------------------------------------------------------- | +| `BAD_REQUEST` | 400 | Invalid input, missing or malformed fields | +| `UNAUTHORIZED` | 401 | No API key or invalid key | +| `FORBIDDEN` | 403 | Valid key, but action not permitted | +| `NOT_FOUND` | 404 | Resource doesn't exist or belongs to another team | +| `METHOD_NOT_ALLOWED` | 405 | Wrong HTTP method for this endpoint | +| `NOT_UNIQUE` | 409 | Resource with this identifier already exists | +| `RATE_LIMITED` | 429 | Too many requests, check the `Retry-After` header | +| `INTERNAL_SERVER_ERROR`| 500 | Unexpected server error, retry with backoff | diff --git a/apps/docs/guides/google-oauth.mdx b/apps/docs/guides/google-oauth.mdx new file mode 100644 index 0000000..b165a52 --- /dev/null +++ b/apps/docs/guides/google-oauth.mdx @@ -0,0 +1,80 @@ +--- +title: Google OAuth Authentication +description: "Sign in to ByteSend using your Google account" +icon: google +--- + +## Overview + +ByteSend supports Google OAuth, allowing you to sign in with your existing Google account. No separate password required. + +## Benefits + +- **Single Sign-On**: Use your Google account to access ByteSend +- **Automatic profile sync**: Your name and profile picture are pulled from Google +- **Account linking**: Connect Google to an existing ByteSend account +- **Secure**: Backed by Google's authentication infrastructure + +## Signing In with Google + +1. Visit [ByteSend](https://bytesend.cloud) +2. Click **Sign in with Google** on the login page +3. Select your Google account or enter your Google email +4. Review the permissions and click **Allow** +5. You'll be redirected back to the ByteSend dashboard + +### First-time sign-in + +On your first Google OAuth login, ByteSend will: +- Create a new account linked to your Google email +- Import your display name and profile picture from Google +- Set up a default workspace for your account + +### Linking to an existing account + +If you already have a ByteSend account with the same email address, Google OAuth will automatically link to it. You can then use either your original sign-in method or Google OAuth interchangeably. + +## Permissions Requested + +ByteSend only requests the minimum permissions needed: + +- **Profile**: Your name and profile picture +- **Email**: Your Google email address to identify your account + +ByteSend never requests access to your Gmail messages, Google Drive, or any other Google services. + +## Security + +- ByteSend stores OAuth tokens securely and never exposes them in API responses or logs +- Tokens are refreshed automatically, so you stay signed in without re-authorizing +- Signing out of ByteSend does not sign you out of Google + +## Revoking Access + +To disconnect ByteSend from your Google account: + +1. Go to [Google Account → Security → Third-party apps](https://myaccount.google.com/connections) +2. Find "ByteSend" and click **Remove access** +3. You'll no longer be able to sign in with Google until you re-authorize + +After revoking, you can still sign in via email magic link if your account has the same email address. + +## Troubleshooting + + + + Google OAuth must be enabled by the platform. On self-hosted ByteSend instances, the administrator needs to configure `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` environment variables. Contact your instance admin. + + + Clear your browser cookies and try again. If the error persists, make sure the Google account has email verification enabled and is not a Google Workspace account with admin restrictions. + + + Contact [ByteSend Support on Discord](https://discord.gg/Bg3Sf5fqa4) and include both email addresses. We can manually unlink and re-link accounts. + + + +## Next Steps + +- [Create your first API key](/guides/api-authentication) +- [Add a verified domain](https://bytesend.cloud/domains) +- [Send your first email](/api-reference/emails/send-email) diff --git a/apps/docs/guides/rate-limits.mdx b/apps/docs/guides/rate-limits.mdx new file mode 100644 index 0000000..bbb45c6 --- /dev/null +++ b/apps/docs/guides/rate-limits.mdx @@ -0,0 +1,131 @@ +--- +title: Rate Limits +description: "Understand ByteSend API rate limits, headers, and how to handle 429 responses" +icon: gauge +--- + +## Overview + +ByteSend enforces rate limits on API requests to ensure fair usage and platform stability. Rate limiting is applied per team per second and is measured using a sliding window. + + +Rate limits only apply to the cloud-hosted version of ByteSend. Self-hosted deployments have no rate limiting. + + +--- + +## How Rate Limiting Works + +Each API request increments a counter scoped to your team. The counter resets every **1 second**. If your team exceeds the allowed number of requests within that window, subsequent requests receive a `429 RATE_LIMITED` response until the window resets. + +Your rate limit is determined by your plan and is set per team at the account level. + +--- + +## Rate Limit Headers + +Every API response includes these headers so you can monitor your usage in real time: + +| Header | Description | +| ------------------- | ------------------------------------------------------------------ | +| `X-RateLimit-Limit` | Maximum requests allowed per second for your team | +| `X-RateLimit-Remaining` | Requests remaining in the current second window | +| `X-RateLimit-Reset` | Unix timestamp (seconds) when the current window resets | +| `Retry-After` | Seconds to wait before retrying (only present on `429` responses) | + +``` +X-RateLimit-Limit: 10 +X-RateLimit-Remaining: 7 +X-RateLimit-Reset: 1750000042 +``` + +--- + +## Handling 429 Responses + +When you receive a `429 RATE_LIMITED` response, wait for the number of seconds specified in the `Retry-After` header before retrying: + +```json +{ + "error": { + "code": "RATE_LIMITED", + "message": "Rate limit exceeded. Try again in 1 seconds." + } +} +``` + +### Retry with backoff (TypeScript) + +```typescript +async function sendWithRetry(payload: object, maxRetries = 3): Promise { + for (let attempt = 0; attempt <= maxRetries; attempt++) { + const response = await fetch("https://bytesend.cloud/api/v1/emails", { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.BYTESEND_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (response.status !== 429) return response; + + if (attempt === maxRetries) throw new Error("Max retries reached"); + + const retryAfter = parseInt(response.headers.get("Retry-After") ?? "1", 10); + await new Promise((r) => setTimeout(r, retryAfter * 1000)); + } + + throw new Error("Unreachable"); +} +``` + +### Retry with backoff (Python) + +```python +import time +import requests + +def send_with_retry(payload: dict, max_retries: int = 3) -> dict: + headers = { + "Authorization": f"Bearer {os.environ['BYTESEND_API_KEY']}", + "Content-Type": "application/json", + } + for attempt in range(max_retries + 1): + response = requests.post( + "https://bytesend.cloud/api/v1/emails", + json=payload, + headers=headers, + ) + if response.status_code != 429: + return response.json() + if attempt == max_retries: + raise Exception("Max retries reached") + retry_after = int(response.headers.get("Retry-After", 1)) + time.sleep(retry_after) +``` + +--- + +## Best Practices + + + + Use the [batch email endpoint](/api-reference/emails/batch-email) to send up to 100 emails in a single API call instead of making one request per recipient. + + + Check `X-RateLimit-Remaining` on each response. When it drops to 0, wait until `X-RateLimit-Reset` before sending the next request. + + + For large campaigns, use a background job queue (BullMQ, Redis Queue, etc.) to pace your API calls rather than sending all at once in a tight loop. + + + Always respect the `Retry-After` header. Immediate retries on `429` responses won't succeed and waste your quota window. + + + +--- + +## Need a Higher Limit? + +If your use case requires a higher rate limit than your plan provides, contact us on [Discord](https://discord.gg/Bg3Sf5fqa4) and we'll work something out. diff --git a/apps/docs/introduction.mdx b/apps/docs/introduction.mdx index 137ed1b..1ea33c4 100644 --- a/apps/docs/introduction.mdx +++ b/apps/docs/introduction.mdx @@ -47,20 +47,36 @@ ByteSend is a powerful, open-source email delivery platform designed for develop ## Authentication & Security + + Generate API keys and authenticate your requests. + + - Sign in securely with your GitHub account. + Sign in with your GitHub account. + Sign in with your Google account. + + + - Learn how to authenticate API requests with your API key. + Sign in with your Discord account. @@ -122,23 +138,23 @@ Set up real-time notifications for your team across multiple platforms: -## Integration Guides +## Sending & Campaigns - Receive real-time notifications about email events. + Organize subscribers into lists and manage contacts. - Build beautiful emails with React Email and ByteSend. + Send newsletters and marketing emails to your contact lists. - Implement compliant double opt-in flows. + Verify new subscribers before adding them to your list. - Create personalized email campaigns at scale. + Use contact properties to personalize campaign content. + + + +## Deliverability + + + + Keep bounce and complaint rates healthy to stay out of spam. + + + + Build beautiful emails with React Email and ByteSend. + + + +## Integration Guides + + + + Receive real-time event notifications. + + + + Understand and handle every API error code. + + + + Understand API rate limits and retry strategies. diff --git a/apps/docs/self-hosting/smtp-server.mdx b/apps/docs/self-hosting/smtp-server.mdx index fabf648..5aff40f 100644 --- a/apps/docs/self-hosting/smtp-server.mdx +++ b/apps/docs/self-hosting/smtp-server.mdx @@ -92,16 +92,8 @@ The official ByteSend SMTP relay image is available on DockerHub as `bytesend/sm 2. **Start the server:** ```bash - # With environment variables - BYTESEND_BASE_URL=https://your-instance.com \ - SMTP_TLS_MODE=manual \ docker compose up -d ``` - - Or edit a `.env` file and run: - ```bash - docker compose --env-file .env up -d - ``` 3. **Verify it's running:** ```bash @@ -151,8 +143,8 @@ If you prefer to build the image locally: 1. **Clone and install:** ```bash - git clone https://github.com/NodeByteLTD/ByteSend.git - cd ByteSend/apps/smtp-server + git clone https://github.com/NodeByteLTD/ByteSend.git + cd ByteSend/apps/smtp-server npm install ``` @@ -161,25 +153,12 @@ If you prefer to build the image locally: npm run build ``` -3. **Set environment variables:** - Copy and configure the example file: +3. **Set environment variables** by copying and editing the example file: ```bash cp .env.example .env - # Edit .env with your settings nano .env ``` - Or set them in your shell: - ```bash - export NODE_ENV=production - export SMTP_AUTH_USERNAME=bytesend - export BYTESEND_BASE_URL=https://bytesend.cloud - export SMTP_TLS_MODE=none - # For manual TLS: - # export SMTP_TLS_CERT_PATH=/path/to/fullchain.pem - # export SMTP_TLS_KEY_PATH=/path/to/privkey.pem - ``` - 4. **Run:** ```bash node dist/server.js @@ -311,7 +290,9 @@ transporter.sendMail({ To run the ByteSend SMTP Server as a systemd service on Linux (without Docker): -**If using `SMTP_TLS_MODE=manual`:** Before proceeding with this setup, ensure your certificate files exist and that the `smtp` user will have read access (see **step 4** below). The service will fail to start if it can't read the certificate. + +**If using `SMTP_TLS_MODE=manual`:** Before proceeding with this setup, ensure your certificate files exist and that the `smtp` user will have read access (see **step 4** below). The service will fail to start if it can't read the certificate. + ### 1. Create a systemd service file @@ -379,7 +360,9 @@ sudo -u smtp cp /opt/bytesend-smtp/apps/smtp-server/.env.example /opt/bytesend-s sudo nano /opt/bytesend-smtp/apps/smtp-server/.env # Edit with your settings ``` -The `HOME=/opt/bytesend-smtp` prefix is needed if Node.js is installed via snap, which restricts home directory access to `/home`. If you get "home directories outside of /home needs configuration", use this workaround or install Node.js via `apt` or `nvm` instead. + +The `HOME=/opt/bytesend-smtp` prefix is needed if Node.js is installed via snap, which restricts home directory access to `/home`. If you get "home directories outside of /home needs configuration", use this workaround or install Node.js via `apt` or `nvm` instead. + Alternatively, you can set environment variables directly in the systemd service file (see step 1). Using `.env` is cleaner for configuration management. diff --git a/apps/web/src/app/banned/page.tsx b/apps/web/src/app/banned/page.tsx index 5d6302b..23f728b 100644 --- a/apps/web/src/app/banned/page.tsx +++ b/apps/web/src/app/banned/page.tsx @@ -1,46 +1,63 @@ +import Image from "next/image"; import Link from "next/link"; +import { ShieldOff } from "lucide-react"; import { FaDiscord } from "react-icons/fa6"; export default function BannedPage() { return ( -
- {/* Background orbs */} -
-
+
+
+
-
-
- - Ban - - - Ban - +
+
+
+ +
+
+

+ Account suspended +

+

+ Your access to ByteSend has been restricted +

+
-
-

- Your account has been suspended -

-

- Access to ByteSend has been restricted. If you believe this is a - mistake, please reach out to us on Discord and we'll look into it. -

-
+
+
+

+ This account has been suspended for violating our{" "} + + Acceptable Use Policy + + . If you believe this is a mistake, please reach out to our team. +

+
-
- - - Join our Discord - +
+ + + Appeal on Discord + + + Back to login + +
-

+

© {new Date().getFullYear()} NodeByte LTD

diff --git a/apps/web/src/components/marketing/TopNav.tsx b/apps/web/src/components/marketing/TopNav.tsx index 1293918..61e2f32 100644 --- a/apps/web/src/components/marketing/TopNav.tsx +++ b/apps/web/src/components/marketing/TopNav.tsx @@ -7,8 +7,10 @@ const isCloud = env.NEXT_PUBLIC_IS_CLOUD; export function TopNav() { return ( -
-
+
+ {/* Blur layer is pointer-events-none to prevent iOS Safari from intercepting touches on page content */} +
+
{/* Logo — server-rendered, zero client JS */} ByteSend diff --git a/apps/web/src/server/auth.ts b/apps/web/src/server/auth.ts index 0aece91..e5ef9c6 100644 --- a/apps/web/src/server/auth.ts +++ b/apps/web/src/server/auth.ts @@ -220,11 +220,12 @@ function getProviders() { export const authOptions: NextAuthOptions = { callbacks: { signIn: async ({ user, account, profile, credentials }) => { - // Block banned users from signing in - const userId = typeof user.id === "string" ? parseInt(user.id, 10) : user.id; - if (userId) { + // Block banned users from signing in. + // Look up by email — user.id is the OAuth provider's ID for OAuth sign-ins + // (e.g. a Discord snowflake that overflows INT4), not the database user ID. + if (user.email) { const dbUser = await db.user.findUnique({ - where: { id: userId }, + where: { email: user.email }, select: { isBanned: true }, }); if (dbUser?.isBanned) {