Sendpit Documentation

Complete technical reference for integrating email testing into your development workflow.

Sendpit Developer Documentation

1. Introduction

Sendpit is an email testing platform designed for development and QA workflows. It provides isolated SMTP mailboxes that capture outgoing emails, allowing teams to test email functionality without sending messages to real recipients.

Sendpit offers a REST API for programmatic access to messages, search, webhooks, and inbox management. Its authenticated browser UI also uses Reverb to refresh an open mailbox in real time.

Quick Start: If you just need to configure SMTP and send test emails, see the Integration Guide for language-specific examples (PHP, Node.js, Python, Ruby, Java, Go).

Who This Documentation Is For

  • Backend developers integrating email sending into applications
  • QA engineers validating email content and delivery
  • DevOps teams setting up CI/CD pipelines with email verification
  • Technical teams building automation around email testing

Common Use Cases

  • Development Testing: Capture emails from local or staging environments
  • CI/CD Integration: Verify transactional emails as part of automated test suites using the REST API and Wait endpoint
  • QA Verification: Inspect email content, headers, attachments, and spam scores via API
  • Webhook-Driven Automation: Trigger external workflows when emails are received
  • Inbox Management: Create and delete mailboxes programmatically via the API

2. Getting Started

Creating an Organization

  1. Sign up at https://sendpit.com/register
  2. An organization is automatically created for your account
  3. You become the organization owner with full administrative access

Organizations are the top-level container for all resources. Team members, mailboxes, and billing are scoped to the organization.

Creating a Mailbox

  1. Navigate to Mailboxes in the dashboard
  2. Click Create Mailbox
  3. Enter a descriptive name (e.g., "Staging Environment", "CI Pipeline")
  4. Sendpit generates unique SMTP credentials automatically

Each mailbox receives:

  • Username: a unique mailbox SMTP username beginning with mb_
  • Password: a unique mailbox SMTP password
  • SMTP Host: smtp.sendpit.com
  • SMTP Port: 587 or 2525 (STARTTLS), or 465 (implicit TLS)

Use 587 by default. Port 2525 is a non-standard compatibility fallback for networks that block standard submission ports; select it explicitly and keep STARTTLS enabled. IANA assigns TCP/2525 to ms-v-worlds, not SMTP.

Sending a Test Email

Configure your application's SMTP settings:

MAIL_MAILER=smtp
MAIL_HOST=smtp.sendpit.com
MAIL_PORT=587
MAIL_USERNAME=mb_your_mailbox_username
MAIL_PASSWORD=your_mailbox_smtp_password
MAIL_SCHEME=smtp
MAIL_REQUIRE_TLS=true

In Laravel 12, ensure the SMTP mailer in config/mail.php includes 'require_tls' => env('MAIL_REQUIRE_TLS', true) so STARTTLS failure is fatal.

Send an email using your application's normal email functionality. The message will appear in Sendpit within seconds.

Viewing Received Emails

  • Navigate to the mailbox in the dashboard
  • Emails are listed in reverse chronological order
  • Click any email to view full content, headers, and attachments

3. Authentication & API Access

API Token Authentication

Sendpit uses Bearer token authentication via Sanctum. All API requests must include a valid token in the Authorization header:

Authorization: Bearer 123|your-plain-text-token

Creating API Tokens

  1. Open the Sendpit dashboard
  2. Navigate to the mailbox you want API access for
  3. Go to Settings > API Tokens
  4. Click Create Token
  5. Copy and securely store the token - it is only shown once

Token Scopes

Tokens are scoped to control access. There are two scope types:

Scope Format Access
Mailbox mailbox:{id} Messages, search, wait, webhooks, attachments, and inspections for that mailbox
Organization organization:{id} Inbox CRUD (list, create, delete mailboxes) for that organization

A token carries exactly one scope. The dashboard currently creates mailbox-scoped tokens only. Organization-scoped tokens are not yet available through a self-service dashboard flow, so inbox-management endpoints should not be part of a self-service integration.

Plan Gating

API access is gated by your organization's plan:

Plan API Access Rate Limit
Free No -
Basic Yes 60 requests/minute
Pro Yes 120 requests/minute
Max Yes 300 requests/minute

Free plan tokens receive a 403 error on all API endpoints.

SMTP Authentication

SMTP access uses per-mailbox credentials. Each mailbox has independent credentials that:

  • Are generated automatically on mailbox creation
  • Can be regenerated at any time (invalidates previous credentials)
  • Are stored encrypted at rest

4. Mailboxes

What a Mailbox Represents

A mailbox is an isolated SMTP endpoint that:

  • Accepts emails via SMTP on ports 587/2525 (STARTTLS) or 465 (implicit TLS)
  • Stores messages for later inspection
  • Can trigger webhooks on email receipt
  • Has its own access control (team members can be granted per-mailbox access)

Address Format

Emails sent to a mailbox can use any address format:

anything@sendpit.com          # Routed by SMTP credentials
user@yourdomain.com           # Captured based on SMTP auth
noreply@staging.example.org   # Any sender/recipient works

The routing is determined by SMTP authentication, not the email address.

Per-Mailbox Behavior

Each mailbox maintains:

  • Independent email storage
  • Separate webhook configurations
  • Individual access control lists
  • Isolated credential sets

Lifecycle Considerations

  • Deletion: Deleting a mailbox permanently removes all associated emails
  • Credential Regeneration: Invalidates existing SMTP credentials immediately
  • Plan Downgrade: Excess mailboxes become read-only; new emails are rejected

5. Emails & Attachments

Compact Schema (List View)

When listing messages, each email includes these fields:

Field Type Description
id string Unique message identifier
message_id string|null RFC 5322 Message-ID header
from object {address, name} - sender
to array [{address, name}] - recipients
cc array [{address, name}] - CC recipients
subject string Email subject line
received_at string ISO 8601 timestamp
size_bytes integer|null Total message size in bytes
is_read boolean Whether the email has been viewed
has_attachments boolean Whether attachments are present
attachment_count integer Number of attachments
has_codes boolean Whether OTP/verification codes were extracted
has_links boolean Whether links were extracted
spam_score number|null Spam analysis score
spam_threshold number|null Threshold used by an exact or estimated result
is_spam boolean|null Exact classification; null while unknown or estimated
spam_status string not_analyzed, pending, estimated, clean, spam, failed, or skipped

Full Schema (Detail View)

The detail view includes all compact fields plus:

Field Type Description
in_reply_to string|null In-Reply-To header value
bcc array [{address, name}] - BCC recipients
is_starred boolean User-applied star flag
labels array Applied label IDs
bodies.text string|null Plain text body
bodies.html string|null HTML body
bodies.text_size_bytes integer|null Text body size
bodies.html_size_bytes integer|null HTML body size
headers object Full parsed email headers
attachments array See attachment object below
extractions.codes array Extracted OTP/verification codes
extractions.links array Extracted links
spam.score number|null Spam score
spam.threshold number|null Spam threshold
spam.is_spam boolean|null Spam classification
spam.is_analyzed boolean Whether spam analysis completed
spam.status string Explicit spam-analysis state
spam.analyzed_at string|null ISO 8601 analysis timestamp
content_hash string|null Content hash for deduplication

Attachment Object

Each attachment in the attachments array:

Field Type Description
index integer Zero-based index
filename string|null Original filename
content_type string|null MIME type
size_bytes integer|null File size in bytes
disposition string|null MIME disposition (attachment or inline)
content_id string|null Normalized Content-ID for inline/related parts
download_url string API URL to download the file

Attachment Access

  • Attachments are accessible only through authenticated API requests
  • Download URLs return the extracted attachment bytes directly after authorization
  • Attachments are not publicly accessible

6. Webhooks

Overview

Webhooks provide real-time HTTP notifications when emails are received, deleted, or cleared. Instead of polling, your application receives a POST request immediately after an event occurs.

Availability & Plans

Plan Webhook Access Webhooks per Mailbox
Free Not available 0
Basic Not available 0
Pro Available 5
Max Available 20

Creating a Webhook

Webhooks can be created via the dashboard or the REST API (see Webhook CRUD in the API Reference).

Via Dashboard:

  1. Navigate to a mailbox in the dashboard
  2. Open Settings > Webhooks
  3. Click Add Webhook
  4. Enter your HTTPS endpoint URL
  5. Optionally add a description
  6. Save the webhook

Upon creation, Sendpit generates a unique signing secret. Store this secret securely - it's required for signature verification.

Requirements:

  • Endpoint must use HTTPS (HTTP is rejected)
  • Endpoint must respond within 10 seconds
  • Endpoint must return a 2xx status code

Event Types

Event Description Payload Version
message.received New email arrives v1, v2
message.deleted Single message deleted v2 only
messages.cleared All messages cleared from mailbox v2 only

Webhook Payload

New webhooks default to payload version 2. This is the payload sent for a message.received event:

{
  "id": "1742049138000-0",
  "type": "message.received",
  "mailbox_id": 15,
  "occurred_at": "2026-03-15T14:32:18+00:00",
  "data": {
    "id": "65a1b2c3d4e5f67890abcdef",
    "message_id": "<abc123@mail.example.com>",
    "from": {
      "address": "sender@example.com",
      "name": null
    },
    "to": [{
      "address": "recipient@yourdomain.com",
      "name": null
    }],
    "cc": [],
    "subject": "Order Confirmation #12345",
    "received_at": "2026-03-15T14:32:17+00:00",
    "size_bytes": 18432,
    "is_read": false,
    "has_attachments": false,
    "attachment_count": 0
  }
}

Version 1 remains available for existing integrations and uses the legacy event, timestamp, mailbox, and email fields. The payload contains metadata only. Email body content and attachments are not included. Use the REST API to retrieve full message content.

Security & Signature Verification

Every webhook request includes cryptographic signatures for verification.

Headers Sent:

Header Description Format Example
X-Sendpit-Signature HMAC-SHA256 signature for payload verification sha256=<hex> sha256=a1b2c3...
X-Sendpit-Timestamp Unix timestamp when request was sent Integer (seconds) 1705329138
X-Sendpit-Webhook-Id Webhook configuration ID from your dashboard Integer 42
X-Sendpit-Event Event type that triggered the webhook String message.received
Content-Type Payload content type MIME type application/json
User-Agent Sendpit webhook client identifier String Sendpit-Webhook/1.0

Signature Verification (Recommended):

import hmac
import hashlib
import time

def verify_signature(secret: str, timestamp: str, payload: str, signature: str) -> bool:
    # Check timestamp is within 5 minutes (replay protection)
    current_time = int(time.time())
    request_time = int(timestamp)
    if abs(current_time - request_time) > 300:
        return False

    # Compute expected signature
    message = f"{timestamp}.{payload}"
    expected = "sha256=" + hmac.new(
        secret.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(expected, signature)
const crypto = require('crypto');

function verifySignature(secret, timestamp, payload, signature) {
  // Check timestamp is within 5 minutes
  const currentTime = Math.floor(Date.now() / 1000);
  const requestTime = parseInt(timestamp, 10);
  if (Math.abs(currentTime - requestTime) > 300) {
    return false;
  }

  // Compute expected signature
  const message = `${timestamp}.${payload}`;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex');

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}
function verifySignature(string $secret, string $timestamp, string $payload, string $signature): bool
{
    // Check timestamp is within 5 minutes
    $currentTime = time();
    $requestTime = (int) $timestamp;
    if (abs($currentTime - $requestTime) > 300) {
        return false;
    }

    // Compute expected signature
    $message = $timestamp . '.' . $payload;
    $expected = 'sha256=' . hash_hmac('sha256', $message, $secret);

    // Constant-time comparison
    return hash_equals($expected, $signature);
}

Delivery, Retries, and Failure Behavior

Retry Schedule:

If delivery fails, Sendpit retries with exponential backoff:

Attempt Delay After Failure
1 Immediate
2 1 minute
3 5 minutes
4 15 minutes
5 1 hour

Retriable Status Codes: 408, 429, 500, 502, 503, 504

Retriable Failures: Network and timeout failures, plus the retriable HTTP status codes above. Other 4xx responses fail immediately.

Auto-Disable Behavior: After 10 consecutive failures, the webhook is automatically disabled. Re-enable it via the dashboard or the API.

Webhook Logs & Retention

Each webhook delivery attempt is logged with timestamp, HTTP status code, response time, error message, and delivery status (success, failed, retrying).

Retention: Logs are retained for 30 days and then permanently deleted.


7. REST API Reference

Base URL

https://sendpit.com/api/v1

Required Headers

Header Value Required
Authorization Bearer {token} Yes
Accept application/json Recommended
Content-Type application/json For POST/PUT requests

Error Responses

Endpoint-specific operational errors commonly use this structure:

{
  "error": {
    "code": "not_found",
    "message": "Message not found.",
    "details": []
  }
}

Authentication, request validation, and framework rate-limit failures can use Laravel's standard JSON shapes instead:

{"message": "You must be logged in to access this resource."}
{
  "message": "The from.email field is required.",
  "errors": {"from.email": ["The from.email field is required."]}
}

Use the HTTP status and message for all failures. Only rely on error.code when an endpoint explicitly returns the error object.

Error Codes

Code HTTP Status Description
unauthorized 403 Token lacks the required scope
plan_required 403 Feature not available on your plan
plan_limit 403 Plan quota exceeded (e.g., max webhooks)
not_found 404 Resource does not exist or is not accessible
validation_failed 422 Request body or parameters failed validation
conflict 409 Conflicting state (e.g., multiple matches on wait with max_results=1)
idempotency_conflict 409 The idempotency key was already used with a different request body
idempotency_in_progress 409 Another request with the same idempotency key is still running
idempotency_unavailable 503 Idempotency was requested but its reservation dependency is unavailable; retry after Retry-After
rate_limited 429 Rate limit exceeded
server_error 500 Internal server error

Observability Headers

Responses that reach the API observability middleware include:

Header Description Example
X-Request-Id Unique identifier for the request 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
X-Processing-Time-Ms Server-side processing time in milliseconds 42

Include X-Request-Id in support tickets for faster debugging. Early authentication, scope, plan, and throttling failures may not include these headers.

Rate Limiting

Plan limits are enforced in fixed one-minute windows and are shared by all tokens in the same organization. The authenticated fallback bucket is per-token when no organization can be resolved.

Response Headers:

Header Description
X-RateLimit-Limit Maximum requests per minute
X-RateLimit-Remaining Remaining requests in the current window
Retry-After Seconds until the rate limit resets (only on 429 responses)

When rate-limited, the API returns 429 Too Many Requests.

GET /rate-limit passes through the same limiter and counts as one request.

Idempotency

For POST /messages and DELETE operations, you can include an X-Idempotency-Key header to ensure the operation is only processed once, even if retried:

X-Idempotency-Key: my-unique-key-12345

For POST /messages, use a UUID that is unique to one logical submission and send the identical body on every retry. Sendpit binds the key and request fingerprint to the stored Mongo message, so a completed submission can still be replayed if the short-lived Redis completion record is lost. Reusing the key with a different body returns 409 idempotency_conflict.

If Sendpit cannot reserve an idempotency key before storing anything, it returns 503 idempotency_unavailable with Retry-After: 5. No message is accepted in that case; retry the identical request later instead of removing the header.

Cursor Pagination

List endpoints use cursor-based pagination. Pass after, before, and limit query parameters.

Paginated Response Envelope:

{
  "data": [...],
  "meta": {
    "next_cursor": "eyJjIjoiMjAyNi0wMy0xNVQxNDozMjoxNy4wMDArMDA6MDAiLCJpZCI6IjY1YTFiMmMzZDRlNWY2Nzg5MGFiY2RlZiJ9",
    "has_more": true,
    "limit": 25,
    "count": 25
  }
}
Parameter Type Default Description
after string - Cursor to fetch the next page (from meta.next_cursor)
before string - Cursor to fetch the previous page
limit integer 25 Number of items per page (1-100)

Messages

Submit Message

Submit an email into the authenticated mailbox over HTTP. This is an alternative to SMTP and follows the same post-ingest path for live updates, webhooks, and inspection.

POST /messages

This endpoint requires a mailbox-scoped token. Send an X-Idempotency-Key header for retry-safe submissions.

Field Type Required Description
from.email string Yes RFC-valid sender address
from.name string No Sender display name
to array Yes One to 100 recipient addresses
cc, bcc, reply_to array No Up to 100 recipient addresses per field
subject string No Subject, up to 998 characters
html, text string One required HTML or plain-text body, each up to 5 MiB
headers object No Up to 100 custom headers. Structural, Content-*, and DKIM-* headers are rejected.
attachments array No Up to 20 attachments. Each contains filename, content_type, and base64-encoded content.

Example Request:

curl -s -X POST "https://sendpit.com/api/v1/messages" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: 4bd49a77-393c-4741-9d52-57e9e01cedfc" \
  -d '{
    "from": {"email": "app@example.test", "name": "Example App"},
    "to": ["recipient@example.test"],
    "subject": "Welcome",
    "text": "Thanks for signing up."
  }'

Response: 201 Created

{
  "data": {
    "id": "65a1b2c3d4e5f67890abcdef",
    "message_id": "<generated-message-id@sendpit.com>"
  }
}

List Messages

Retrieve messages for the authenticated mailbox with optional filters and cursor pagination.

GET /messages

Query Parameters:

Parameter Type Description
to string Filter by recipient address (substring match, or exact with match=exact)
from string Filter by sender address (substring match, or exact with match=exact)
match string Address filter matching mode: contains (default, substring) or exact (full address). Applies to to/from only
subject string Filter by subject (substring match)
since date Messages received after this date
until date Messages received before this date
has_attachments boolean Filter by attachment presence
unread boolean Filter by read status
after string Pagination cursor
limit integer Results per page (1-100, default 25)

Example Request:

curl -s "https://sendpit.com/api/v1/messages?from=noreply&has_attachments=true&limit=10" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": [
    {
      "id": "65a1b2c3d4e5f67890abcdef",
      "message_id": "<20260315143217.abc123@mail.example.com>",
      "from": {
        "address": "noreply@example.com",
        "name": null
      },
      "to": [
        {
          "address": "user@staging.myapp.com",
          "name": null
        }
      ],
      "cc": [],
      "subject": "Your invoice #INV-2026-0042 is ready",
      "received_at": "2026-03-15T14:32:17+00:00",
      "size_bytes": 18432,
      "is_read": false,
      "has_attachments": true,
      "attachment_count": 1,
      "has_codes": false,
      "has_links": true,
      "spam_score": 1.2,
      "spam_threshold": 5.0,
      "is_spam": false,
      "spam_status": "clean"
    }
  ],
  "meta": {
    "next_cursor": "eyJjIjoiMjAyNi0wMy0xNVQxNDozMjoxNy4wMDArMDA6MDAiLCJpZCI6IjY1YTFiMmMzZDRlNWY2Nzg5MGFiY2RlZiJ9",
    "has_more": true,
    "limit": 10,
    "count": 10
  }
}

Get Message

Retrieve full detail for a single message. Add ?include=attachments to populate the attachments array; without it, attachment metadata is omitted to avoid parsing raw MIME on every detail request.

GET /messages/{id}?include=attachments

Example Request:

curl -s "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef?include=attachments" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": {
    "id": "65a1b2c3d4e5f67890abcdef",
    "message_id": "<20260315143217.abc123@mail.example.com>",
    "from": {
      "address": "noreply@example.com",
      "name": null
    },
    "to": [
      {
        "address": "user@staging.myapp.com",
        "name": null
      }
    ],
    "cc": [],
    "subject": "Your invoice #INV-2026-0042 is ready",
    "received_at": "2026-03-15T14:32:17+00:00",
    "size_bytes": 18432,
    "is_read": true,
    "has_attachments": true,
    "attachment_count": 1,
    "has_codes": false,
    "has_links": true,
    "spam_score": 1.2,
    "is_spam": false,
    "spam_status": "clean",
    "in_reply_to": null,
    "bcc": [],
    "is_starred": false,
    "labels": [],
    "bodies": {
      "text": "Your invoice #INV-2026-0042 is attached.\n\nTotal: $149.99\nDue: March 30, 2026",
      "html": "<html><body><h1>Invoice #INV-2026-0042</h1><p>Total: $149.99</p></body></html>",
      "text_size_bytes": 72,
      "html_size_bytes": 1024
    },
    "headers": {
      "Message-ID": "<20260315143217.abc123@mail.example.com>",
      "Date": "Sat, 15 Mar 2026 14:32:17 +0000",
      "From": "noreply@example.com",
      "To": "user@staging.myapp.com",
      "Subject": "Your invoice #INV-2026-0042 is ready",
      "Content-Type": "multipart/mixed; boundary=\"----=_Part_123\"",
      "X-Mailer": "MyApp/3.2.1"
    },
    "attachments": [
      {
        "index": 0,
        "filename": "invoice-2026-0042.pdf",
        "content_type": "application/pdf",
        "size_bytes": 15360,
        "download_url": "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef/attachments/0"
      }
    ],
    "extractions": {
      "codes": [],
      "links": ["https://myapp.com/invoices/INV-2026-0042"]
    },
    "spam": {
      "score": 1.2,
      "threshold": 5.0,
      "is_spam": false,
      "is_analyzed": true,
      "status": "clean",
      "analyzed_at": "2026-03-15T14:32:18+00:00"
    },
    "content_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
  }
}

Delete Message

Delete a single message by ID.

DELETE /messages/{id}

Example Request:

curl -s -X DELETE "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "X-Idempotency-Key: del-msg-65a1b2c3"

Response: 204 No Content


Delete All Messages

Delete all messages in the authenticated mailbox.

DELETE /messages

Example Request:

curl -s -X DELETE "https://sendpit.com/api/v1/messages" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "X-Idempotency-Key: clear-mailbox-20260315"

Response: 204 No Content


Download Attachment

Download an attachment by message ID and zero-based MIME traversal index.

GET /messages/{id}/attachments/{index}

Example Request:

curl -sL "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef/attachments/0" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -o invoice.pdf

Response: 200 OK with the decoded attachment bytes, MIME type, length, and an attachment content disposition.


Search Messages

Perform a structured search with advanced filters. Supports all list filters plus body content, CC/BCC, headers, extracted codes/links, and spam score ranges.

POST /messages/search

Request Body:

Field Type Description
filters.from string Sender address (substring match, or exact with filters.match=exact)
filters.to string Recipient address (substring match, or exact with filters.match=exact)
filters.cc string CC address (substring match, or exact with filters.match=exact)
filters.bcc string BCC address (substring match, or exact with filters.match=exact)
filters.match string Address filter matching mode: contains (default, substring) or exact (full address). Applies to from/to/cc/bcc only
filters.subject string Subject (substring match)
filters.body string Body content (substring match, max 1000 chars)
filters.headers object Header key-value pairs to match (max 10 headers)
filters.received_after date Messages received after this date
filters.received_before date Messages received before this date
filters.has_attachments boolean Filter by attachment presence
filters.unread boolean Filter by read status
filters.has_codes boolean Filter by extracted OTP/verification codes
filters.has_links boolean Filter by extracted links
filters.spam_score_min number Minimum spam score; negative scores are valid
filters.spam_score_max number Maximum spam score; negative scores are valid
after string Pagination cursor
limit integer Results per page (1-100, default 25)

If filters.spam_score_min is greater than filters.spam_score_max, the API returns a 422 validation response rather than an empty result set.

Example Request:

curl -s -X POST "https://sendpit.com/api/v1/messages/search" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "filters": {
      "from": "noreply@example.com",
      "subject": "verification",
      "has_codes": true,
      "received_after": "2026-03-14T00:00:00Z"
    },
    "limit": 5
  }'

Example Response:

{
  "data": [
    {
      "id": "65a1b2c3d4e5f67890abcdf0",
      "message_id": "<20260315091023.xyz789@mail.example.com>",
      "from": {
        "address": "noreply@example.com",
        "name": null
      },
      "to": [
        {
          "address": "newuser@staging.myapp.com",
          "name": null
        }
      ],
      "cc": [],
      "subject": "Your verification code is 847291",
      "received_at": "2026-03-15T09:10:23+00:00",
      "size_bytes": 4096,
      "is_read": false,
      "has_attachments": false,
      "attachment_count": 0,
      "has_codes": true,
      "has_links": true,
      "spam_score": 0.3,
      "is_spam": false
    }
  ],
  "meta": {
    "next_cursor": null,
    "has_more": false,
    "limit": 5,
    "count": 1
  }
}

Wait (Long-Poll)

Wait for Messages

Long-poll until one or more messages matching your filters arrive, or timeout. This is ideal for CI/CD pipelines and test automation where you need to wait for an email to be delivered.

POST /messages/wait

Plan Requirement: Basic or higher (with wait API enabled).

Request Body:

Field Type Default Description
filters.from string - Sender address (substring match, or exact with filters.match=exact)
filters.to string - Recipient address (substring match, or exact with filters.match=exact)
filters.cc string - CC address (substring match, or exact with filters.match=exact)
filters.match string contains Address filter matching mode: contains (substring) or exact (full address). Applies to from/to/cc only
filters.subject string - Subject (substring match)
filters.body string - Body content (substring match)
filters.headers object - Header key-value pairs to match
filters.received_after date - Messages received after this date
filters.received_before date - Messages received before this date
filters.has_attachments boolean - Filter by attachment presence
filters.unread boolean - Filter by read status
timeout integer 15 Seconds to wait (1-30)
max_results integer 10 Maximum messages to return (1-100)
after_cursor string - Only return messages after this cursor

Note: Async-derived filters (has_codes, has_links, spam_score_min, spam_score_max) are not supported in the wait API because they depend on post-processing that may not complete before the timeout. Use POST /messages/search instead.

Example Request:

curl -s -X POST "https://sendpit.com/api/v1/messages/wait" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "filters": {
      "to": "newuser@staging.myapp.com",
      "subject": "Welcome"
    },
    "timeout": 20,
    "max_results": 1
  }'

Response Codes:

Status Meaning
200 Matching messages found
204 Timeout - no matching messages arrived
409 Conflict - multiple messages matched with max_results=1

Example 200 Response:

{
  "data": [
    {
      "id": "65a1b2c3d4e5f67890abcdf1",
      "message_id": "<20260315143500.welcome@mail.example.com>",
      "from": {
        "address": "welcome@example.com",
        "name": null
      },
      "to": [
        {
          "address": "newuser@staging.myapp.com",
          "name": null
        }
      ],
      "cc": [],
      "subject": "Welcome to Example!",
      "received_at": "2026-03-15T14:35:00+00:00",
      "size_bytes": 8192,
      "is_read": false,
      "has_attachments": false,
      "attachment_count": 0,
      "has_codes": true,
      "has_links": true,
      "spam_score": 0.1,
      "is_spam": false
    }
  ],
  "meta": {
    "next_cursor": null,
    "has_more": false,
    "limit": 1,
    "count": 1
  }
}

Example 409 Response:

{
  "error": {
    "code": "conflict",
    "message": "Multiple messages match the filter with max_results=1. Broaden timeout or narrow filters."
  }
}

Inspection Endpoints

These endpoints provide focused access to specific parts of a message. All require a mailbox-scoped token.

Raw Email

Download the raw RFC 2822 .eml file.

GET /messages/{id}/raw
curl -s "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef/raw" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -o message.eml

Response: 200 OK with Content-Type: message/rfc822


Headers

Retrieve parsed email headers as JSON.

GET /messages/{id}/headers
curl -s "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef/headers" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": {
    "Message-ID": "<20260315143217.abc123@mail.example.com>",
    "Date": "Sat, 15 Mar 2026 14:32:17 +0000",
    "From": "noreply@example.com",
    "To": "user@staging.myapp.com",
    "Subject": "Your invoice #INV-2026-0042 is ready",
    "Content-Type": "multipart/mixed; boundary=\"----=_Part_123\"",
    "DKIM-Signature": "v=1; a=rsa-sha256; d=example.com; ...",
    "X-Mailer": "MyApp/3.2.1"
  }
}

Plain Text Body

Retrieve the plain text body.

GET /messages/{id}/body/text
curl -s "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef/body/text" \
  -H "Authorization: Bearer 123|your-plain-text-token"

Response: 200 OK with Content-Type: text/plain; charset=utf-8

Your invoice #INV-2026-0042 is attached.

Total: $149.99
Due: March 30, 2026

HTML Body

Retrieve the HTML body wrapped in JSON (for XSS safety).

GET /messages/{id}/body/html
curl -s "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef/body/html" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": {
    "html": "<html><body><h1>Invoice #INV-2026-0042</h1><p>Total: $149.99</p></body></html>"
  }
}

Attachments List

List all attachments for a message with download URLs.

GET /messages/{id}/attachments
curl -s "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef/attachments" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": [
    {
      "index": 0,
      "filename": "invoice-2026-0042.pdf",
      "content_type": "application/pdf",
      "size_bytes": 15360,
      "disposition": "attachment",
      "content_id": null,
      "download_url": "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef/attachments/0"
    }
  ]
}

Extractions

Retrieve extracted OTP codes and links from the message body.

GET /messages/{id}/extractions
curl -s "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdf0/extractions" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": {
    "codes": ["847291"],
    "links": [
      "https://example.com/verify?token=abc123",
      "https://example.com/unsubscribe"
    ],
    "content_extracted_at": "2026-03-15T09:10:24+00:00"
  }
}

Inspection

Retrieve spam analysis results and message metadata.

GET /messages/{id}/inspection
curl -s "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef/inspection" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": {
    "spam": {
      "score": 1.2,
      "threshold": 5.0,
      "is_spam": false,
      "is_analyzed": true,
      "status": "clean",
      "analyzed_at": "2026-03-15T14:32:18+00:00",
      "analysis_duration_ms": 85
    },
    "metadata": {
      "size_bytes": 18432,
      "text_size_bytes": 72,
      "html_size_bytes": 1024,
      "content_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
      "message_id": "<20260315143217.abc123@mail.example.com>",
      "in_reply_to": null
    }
  }
}

Timeline

Retrieve the processing timeline for a message, showing when each stage of processing occurred.

GET /messages/{id}/timeline
curl -s "https://sendpit.com/api/v1/messages/65a1b2c3d4e5f67890abcdef/timeline" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": [
    {
      "event": "received",
      "at": "2026-03-15T14:32:17+00:00"
    },
    {
      "event": "webhooks_dispatched",
      "at": "2026-03-15T14:32:17+00:00"
    },
    {
      "event": "spam_analyzed",
      "at": "2026-03-15T14:32:18+00:00",
      "duration_ms": 85
    },
    {
      "event": "content_extracted",
      "at": "2026-03-15T14:32:18+00:00"
    }
  ]
}

Timeline Events:

Event Description
received Email was received by Sendpit
webhooks_dispatched Webhook notifications were sent
spam_analyzed Spam analysis completed (includes duration_ms)
content_extracted OTP codes and links were extracted
smart_rules_applied Smart rules were evaluated and applied

Webhook CRUD

Manage webhooks programmatically via the API. Requires a mailbox-scoped token.

List Webhooks

GET /webhooks
curl -s "https://sendpit.com/api/v1/webhooks" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": [
    {
      "id": 42,
      "mailbox_id": 15,
      "endpoint_url": "https://ci.example.com/hooks/sendpit",
      "is_active": true,
      "description": "CI pipeline webhook",
      "event_types": ["message.received"],
      "payload_version": 2,
      "last_triggered_at": "2026-03-15T14:32:18+00:00",
      "last_success_at": "2026-03-15T14:32:18+00:00",
      "last_failure_at": null,
      "consecutive_failures": 0,
      "created_at": "2026-03-01T10:00:00+00:00",
      "updated_at": "2026-03-15T14:32:18+00:00"
    }
  ]
}

Create Webhook

POST /webhooks

Request Body:

Field Type Required Description
endpoint_url string Yes HTTPS URL for webhook delivery (max 2048 chars)
secret string No Signing secret (min 16 chars). Auto-generated if omitted
description string No Human-readable description (max 255 chars)
event_types array No Events to subscribe to (default: ["message.received"])
payload_version integer No Payload format version: 1 or 2 (default: 2)

Valid event types: message.received, message.deleted, messages.cleared

Note: message.deleted and messages.cleared require payload_version: 2.

curl -s -X POST "https://sendpit.com/api/v1/webhooks" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "endpoint_url": "https://ci.example.com/hooks/sendpit",
    "description": "CI pipeline webhook",
    "event_types": ["message.received", "message.deleted"],
    "payload_version": 2
  }'

Example Response (201 Created):

{
  "data": {
    "id": 43,
    "mailbox_id": 15,
    "endpoint_url": "https://ci.example.com/hooks/sendpit",
    "is_active": true,
    "description": "CI pipeline webhook",
    "event_types": ["message.received", "message.deleted"],
    "payload_version": 2,
    "last_triggered_at": null,
    "last_success_at": null,
    "last_failure_at": null,
    "consecutive_failures": 0,
    "created_at": "2026-03-15T15:00:00+00:00",
    "updated_at": "2026-03-15T15:00:00+00:00"
  }
}

Get Webhook

GET /webhooks/{id}
curl -s "https://sendpit.com/api/v1/webhooks/42" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Response: Same schema as the webhook object shown above.


Update Webhook

PUT /webhooks/{id}

All fields are optional. Only include the fields you want to change.

Field Type Description
endpoint_url string New endpoint URL
secret string New signing secret (min 16 chars)
is_active boolean Enable or disable the webhook
description string|null Updated description
event_types array Updated event subscriptions
payload_version integer 1 or 2
curl -s -X PUT "https://sendpit.com/api/v1/webhooks/42" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "is_active": false,
    "description": "Disabled for maintenance"
  }'

Example Response:

{
  "data": {
    "id": 42,
    "mailbox_id": 15,
    "endpoint_url": "https://ci.example.com/hooks/sendpit",
    "is_active": false,
    "description": "Disabled for maintenance",
    "event_types": ["message.received"],
    "payload_version": 2,
    "last_triggered_at": "2026-03-15T14:32:18+00:00",
    "last_success_at": "2026-03-15T14:32:18+00:00",
    "last_failure_at": null,
    "consecutive_failures": 0,
    "created_at": "2026-03-01T10:00:00+00:00",
    "updated_at": "2026-03-15T16:00:00+00:00"
  }
}

Delete Webhook

DELETE /webhooks/{id}
curl -s -X DELETE "https://sendpit.com/api/v1/webhooks/42" \
  -H "Authorization: Bearer 123|your-plain-text-token"

Response: 204 No Content


Webhook Delivery Logs

Retrieve delivery logs for a specific webhook.

GET /webhooks/{id}/deliveries

Query Parameters:

Parameter Type Default Description
per_page integer 25 Results per page
curl -s "https://sendpit.com/api/v1/webhooks/42/deliveries?per_page=10" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": [
    {
      "id": 1001,
      "webhook_id": 42,
      "event_type": "message.received",
      "status": "success",
      "http_status": 200,
      "duration_ms": 142,
      "attempt_number": 1,
      "error_message": null,
      "triggered_at": "2026-03-15T14:32:18+00:00",
      "delivered_at": "2026-03-15T14:32:18+00:00",
      "created_at": "2026-03-15T14:32:18+00:00"
    },
    {
      "id": 998,
      "webhook_id": 42,
      "event_type": "message.received",
      "status": "failed",
      "http_status": 503,
      "duration_ms": 10012,
      "attempt_number": 3,
      "error_message": "Service Unavailable",
      "triggered_at": "2026-03-14T22:10:05+00:00",
      "delivered_at": null,
      "created_at": "2026-03-14T22:15:05+00:00"
    }
  ]
}

Inboxes (Organization-Scoped)

Manage mailboxes programmatically. Requires an organization-scoped token (organization:{id}).

List Inboxes

GET /inboxes
curl -s "https://sendpit.com/api/v1/inboxes" \
  -H "Authorization: Bearer 456|your-organization-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": [
    {
      "id": 15,
      "name": "Staging Environment",
      "email_address": "mb_example_staging@sendpit.com",
      "smtp_username": "mb_example_staging",
      "created_at": "2026-02-01T10:00:00+00:00",
      "message_count": 247
    },
    {
      "id": 16,
      "name": "CI Pipeline",
      "email_address": "mb_example_ci@sendpit.com",
      "smtp_username": "mb_example_ci",
      "created_at": "2026-03-01T08:30:00+00:00",
      "message_count": 1832
    }
  ]
}

Create Inbox

POST /inboxes

Request Body:

Field Type Required Description
name string Yes Mailbox name (max 255 chars)
curl -s -X POST "https://sendpit.com/api/v1/inboxes" \
  -H "Authorization: Bearer 456|your-organization-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "name": "E2E Test Suite"
  }'

Example Response (201 Created):

{
  "data": {
    "id": 17,
    "name": "E2E Test Suite",
    "email_address": "mb_example_e2e@sendpit.com",
    "smtp_username": "mb_example_e2e",
    "created_at": "2026-03-15T16:30:00+00:00",
    "message_count": 0
  }
}

Get Inbox

GET /inboxes/{id}
curl -s "https://sendpit.com/api/v1/inboxes/15" \
  -H "Authorization: Bearer 456|your-organization-token" \
  -H "Accept: application/json"

Response: Same schema as the inbox object shown above.


Delete Inbox

Permanently deletes a mailbox and all its associated messages.

DELETE /inboxes/{id}
curl -s -X DELETE "https://sendpit.com/api/v1/inboxes/17" \
  -H "Authorization: Bearer 456|your-organization-token"

Response: 204 No Content


Rate Limit

Check Rate Limit Status

Retrieve your current rate limit status. This request passes through the API rate limiter and consumes one request.

GET /rate-limit
curl -s "https://sendpit.com/api/v1/rate-limit" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Accept: application/json"

Example Response:

{
  "data": {
    "limit": 120,
    "remaining": 98,
    "resets_at": "2026-03-15T14:33:17+00:00"
  }
}

8. Browser Inbox Live Refresh (WebSocket)

Sendpit uses the Pusher protocol through Laravel Reverb to refresh an open mailbox page when messages arrive or are removed. This is a session-authenticated browser feature for every plan, not a public API-token WebSocket interface.

Channel

The inbox UI receives events on a private channel bound to the mailbox:

private-mailbox.{id}

This is a session-authenticated browser channel, not a public API-token WebSocket contract.

Events

Event Name Trigger Payload
message.received New email arrives {id, type, mailbox_id, occurred_at, data: {message fields}}
message.deleted Single message deleted {id, type, mailbox_id, occurred_at, data: {id}}
messages.cleared All messages cleared {id, type, mailbox_id, occurred_at, data: {count}}

Browser Authentication Flow

  1. Client connects to the WebSocket server: wss://sendpit.com/app/{APP_KEY}
  2. Client subscribes to the mailbox's private channel
  3. The server sends an auth challenge
  4. The signed-in browser POSTs to /broadcasting/auth with its session cookie, CSRF context, channel name, and socket ID
  5. The server verifies mailbox access and returns a signed channel token
  6. Client completes the subscription

Browser Clients (Laravel Echo)

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Echo = new Echo({
    broadcaster: 'reverb',
    key: 'your-reverb-app-key',
    wsHost: 'sendpit.com',
    wsPort: 443,
    wssPort: 443,
    forceTLS: true,
    enabledTransports: ['ws', 'wss'],
    authEndpoint: '/broadcasting/auth',
});

window.Echo.private(`mailbox.${mailboxId}`)
    .listen('.message.received', (event) => {
        console.log('New message:', event);
    })
    .listen('.message.deleted', (event) => {
        console.log('Message deleted:', event);
    })
    .listen('.messages.cleared', (event) => {
        console.log('Messages cleared:', event);
    });

API and CI Clients

API tokens cannot authenticate to this browser channel. Sendpit does not register a Sanctum broadcasting-auth route. API and CI clients should use the Wait API for asynchronous message arrival, or customer webhooks where the selected plan supports them.


9. Plans & Limits

Feature Availability by Plan

Feature Free Basic Pro Max
Mailboxes 1 3 Unlimited Unlimited
Team Members 3 5 Unlimited Unlimited
Stored emails (org-wide) 200 1,000 2,500 15,000
Email Retention 7 days 14 days 60 days 60 days
REST API No Yes (60 rpm) Yes (120 rpm) Yes (300 rpm)
Wait API No Yes (2 concurrent/token) Yes (5 concurrent/token) Yes (10 concurrent/token)
Webhooks No No Yes (5/mailbox) Yes (20/mailbox)
Browser inbox live refresh Yes Yes Yes Yes
Spam Rule Breakdown No Yes Yes Yes
Link Checking No Yes Yes Yes
Enhanced Header Inspector No No Yes Yes
Share Links No No Yes Yes
Email Forwarding No No Yes Yes
Smart Rules No No Yes (100 rules) Yes (500 rules)

Behavior on Downgrade

When downgrading to a lower plan:

  • Excess mailboxes: Become read-only; new emails are rejected
  • Excess team members: Retain access but no new invitations allowed
  • Webhooks: Disabled if new plan doesn't support them; existing configs are preserved but inactive
  • API tokens: Tokens remain but API requests return 403 if API access is removed
  • Emails over retention: Purged according to new retention period

Limit Enforcement

Limits are checked at:

  • Mailbox creation time
  • User invitation time
  • Email ingestion time
  • Webhook creation time
  • API request time (rate limits)

Attempting to exceed limits returns a 403 error with the plan_limit error code.

The email limit is a stored-email capacity: the number of captured messages your organization currently holds across all its mailboxes, from SMTP and API submissions alike. New messages are rejected while you are at capacity (SMTP with a permanent 5.2.2 response, the API with plan_limit); deleting messages or retention cleanup frees space immediately, and billing renewals do not reset the count. Simultaneous submissions at the final available slot can briefly exceed the limit because enforcement uses count-then-insert; the hourly cleanup keeps the newest messages and removes the oldest excess.


10. Error Handling & Status Codes

Common Errors

Scenario HTTP Status Error Code Message
Missing or invalid token 401 Not guaranteed Check the response message
Token lacks required scope 403 unauthorized Insufficient scope
API not available on plan 403 plan_required Your plan does not include API access
Mailbox limit reached 403 plan_limit Maximum mailboxes for your plan
Webhook limit reached 403 plan_limit Maximum webhooks per mailbox
Stored-email capacity reached 403 plan_limit Organization is at its stored-email capacity; delete stored emails or upgrade (idempotent retries of an already-stored message still replay their original 201)
Message not found 404 not_found Message not found
Invalid request body 422 validation_failed Validation details in error.details
Rate limit exceeded 429 rate_limited Too many requests
Invalid SMTP credentials N/A - SMTP authentication failed

Developer Recommendations

  1. Check plan limits before operations: Use GET /rate-limit to check current rate limit status
  2. Handle 403 gracefully: Display upgrade prompts or notify administrators
  3. Implement retry logic for transient failures: Use exponential backoff for 429 and 5xx responses
  4. Log error responses: Include X-Request-Id for debugging with support
  5. Use cursor pagination: Prefer after/limit over offset pagination for reliable iteration

11. Security Model

Tenant Isolation

  • Organizations are fully isolated from each other
  • Mailbox data is scoped to the owning organization
  • API tokens are scoped to a single mailbox or organization
  • Team members only see resources they have explicit access to

Attachment Access Control

Attachments are protected by:

  • Authentication requirement (valid API token or logged-in user)
  • Mailbox access verification (token must be scoped to the correct mailbox)
  • Signed URLs with 1-hour expiration for downloads

Attachments are not publicly accessible.

Webhook Signing Guarantees

  • Secrets are generated using cryptographically secure random bytes (256-bit)
  • Secrets are encrypted at rest using AES-256
  • Signatures use HMAC-SHA256
  • Timestamp validation prevents replay attacks
  • Secrets can be regenerated at any time (invalidates old signatures)

12. CI / Automation Examples

GitHub Actions with REST API

name: Email Integration Tests

on:
  push:
    branches: [main, develop]

jobs:
  email-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Send test email via SMTP
        run: |
          # Your application sends an email here
          npm run send-welcome-email -- --to test@staging.example.com

      - name: Wait for email via API
        run: |
          RESPONSE=$(curl -s -X POST "https://sendpit.com/api/v1/messages/wait" \
            -H "Authorization: Bearer ${{ secrets.SENDPIT_API_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d '{
              "filters": {
                "to": "test@staging.example.com",
                "subject": "Welcome"
              },
              "timeout": 30,
              "max_results": 1
            }')

          echo "$RESPONSE" | jq .

          # Verify the email arrived
          SUBJECT=$(echo "$RESPONSE" | jq -r '.data[0].subject')
          if [ "$SUBJECT" != "Welcome to Our Platform" ]; then
            echo "Unexpected subject: $SUBJECT"
            exit 1
          fi

      - name: Extract OTP code
        run: |
          MESSAGE_ID=$(echo "$RESPONSE" | jq -r '.data[0].id')
          EXTRACTIONS=$(curl -s "https://sendpit.com/api/v1/messages/$MESSAGE_ID/extractions" \
            -H "Authorization: Bearer ${{ secrets.SENDPIT_API_TOKEN }}")

          OTP=$(echo "$EXTRACTIONS" | jq -r '.data.codes[0]')
          echo "Extracted OTP: $OTP"

Webhook-Driven Test Workflow

# test_webhook_handler.py
import time
from flask import Flask, request
import threading

app = Flask(__name__)
received_emails = []
email_event = threading.Event()

@app.route('/webhook', methods=['POST'])
def webhook():
    payload = request.get_json()
    received_emails.append(payload)
    email_event.set()
    return '', 200

def wait_for_email(timeout=30):
    """Block until an email is received or timeout."""
    email_event.clear()
    if email_event.wait(timeout):
        return received_emails[-1]
    raise TimeoutError("No email received within timeout")

# In your test
def test_welcome_email():
    register_user("test@example.com")
    email = wait_for_email(timeout=10)
    assert email['data']['subject'] == "Welcome to Our Platform"
    assert email['data']['to'][0]['address'] == "test@example.com"

Local Development with ngrok

Step 1: Start ngrok

ngrok http 3000

Step 2: Configure webhook in Sendpit

Use the ngrok HTTPS URL as your webhook endpoint:

curl -s -X POST "https://sendpit.com/api/v1/webhooks" \
  -H "Authorization: Bearer 123|your-plain-text-token" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint_url": "https://a1b2c3d4.ngrok-free.app/webhooks/sendpit",
    "description": "Local dev webhook"
  }'

13. Versioning & Changelog Policy

API Versioning

The API is versioned via the URL path (/api/v1). The current version is v1.

  • Additive changes (new fields, new endpoints): Deployed without notice. Your code should ignore unknown fields.
  • Breaking changes (removed fields, type changes, removed endpoints): Announced 30 days in advance via email to organization owners. A new API version will be introduced.

Webhook Payload Versioning

Webhook payloads support version 1 and 2. When creating or updating a webhook, specify the payload_version field:

  • Version 1: Supports message.received events only.
  • Version 2: Supports all event types (message.received, message.deleted, messages.cleared).

Backward Compatibility

We commit to:

  • Not removing existing response fields without notice
  • Not changing field types without notice
  • Maintaining signature algorithm compatibility
  • Supporting deprecated pagination (offset-based) alongside cursor pagination

14. FAQ

Can I retrieve email content via API?

Yes. Use GET /messages/{id} for the full message including bodies and headers. Add ?include=attachments to include attachment metadata, or use the inspection endpoints for focused access to raw email, headers, bodies, attachments, and extractions.

How do I wait for an email in my CI pipeline?

Use POST /messages/wait with your desired filters and a timeout. The endpoint long-polls until a matching email arrives or the timeout expires. See Wait for Messages.

Why doesn't the webhook payload include the email body?

For security and performance reasons. Email bodies can be large and may contain sensitive data. The webhook notifies you of receipt; use the REST API to retrieve full content.

What happens if my webhook endpoint is down?

Sendpit retries delivery up to 5 times over approximately 81 minutes. If all attempts fail, the webhook is logged as failed. After 10 consecutive failures across any deliveries, the webhook is auto-disabled.

What is the difference between mailbox and organization tokens?

Mailbox tokens (mailbox:{id}) provide access to messages, search, wait, webhooks, and attachments for a single mailbox. Organization tokens (organization:{id}) provide access to inbox management (list, create, delete mailboxes) for the entire organization, but are not currently issued through the self-service dashboard.

Are emails permanently stored?

No. Emails are retained according to your plan's retention period (7-60 days), then automatically deleted.

Can I use custom domains?

SMTP credentials work regardless of the sender/recipient domain in emails. The routing is based on SMTP authentication, not email addresses.

What email size limits apply?

SMTP submissions are limited to 25 MiB including attachments. HTTP submissions use separate decoded attachment limits, which are enforced through validation.


15. Support & Feedback

Reporting Issues

For bugs, unexpected behavior, or security concerns:

  • Email: support@sendpit.com
  • Include: Organization ID, mailbox name (not credentials), X-Request-Id header value, timestamp, and reproduction steps

Feature Requests

Submit feature requests through the in-app feedback form or email. We prioritize based on impact on existing workflows, alignment with product direction, and technical feasibility.

Security Vulnerabilities

Report security issues to security@sendpit.com. We follow responsible disclosure practices and will acknowledge receipt within 48 hours.


Last updated: March 2026

Last updated: July 2026

Questions? Contact [email protected]