CI

Assert on real email, in CI.

Your pipeline sends real SMTP, Sendtrap catches it, and one API call tells you whether the right message arrived. This works today against the hosted Cloud — no container, no SDK, no polling loop.

What works today

Everything on this page is live now: a Cloud inbox is a real SMTP endpoint plus a token-authenticated REST API, and that combination is all a CI email test needs. Your app under test sends mail exactly as it would in production; your test asserts over HTTP that the right message arrived, then clears the inbox for the next run.

Pipeline setup

Three pieces of configuration, all injected as environment variables:

  • SMTP credentials — point the app under test at the inbox's SMTP host/port/username/password (from the inbox's Settings page), the same way you'd configure any mail server.
  • The inbox API token — store it as a CI secret (e.g. SENDTRAP_TOKEN); never commit it.
  • A per-run tag — set an X-Sendtrap-Test-Id header on outgoing mail (any mail library lets you add a custom header) so each run can find its own messages. Most CI systems already give you a unique run ID to use.

Wait & assert

Mail arrives asynchronously, so never assert on the first response. Instead of a sleep loop, make one blocking call: POST /assert waits until a matching message arrives (or the timeout hits, capped at 30s) and always returns 200 with a pass/fail flag in the body.

bash
curl -s -X POST https://sendtrap.dev/api/v1/assert \
  -H "Authorization: Bearer $SENDTRAP_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"test_id\": \"$CI_RUN_ID\", \"subject_contains\": \"Welcome\", \"timeout\": 15}" \
  | jq -e '.matched'

# clean up so the next run starts from an empty inbox
curl -s -X DELETE https://sendtrap.dev/api/v1/messages \
  -H "Authorization: Bearer $SENDTRAP_TOKEN"

Prefer GET /messages?wait=15 for the same blocking behaviour on a plain list request. Both are covered in depth — parameters, response shapes, rate limits — in the API reference, along with ready-made PHPUnit, Jest/Vitest and pytest examples.

GitHub Actions

The same recipe as a workflow step — GITHUB_RUN_ID is the per-run tag, and the token comes from a repository secret:

.github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    env:
      SENDTRAP_TOKEN: ${{ secrets.SENDTRAP_TOKEN }}
    steps:
      - uses: actions/checkout@v4

      # ... set up and run your app's test suite; the app sends mail
      # through the inbox's SMTP credentials, tagged with
      # X-Sendtrap-Test-Id: $GITHUB_RUN_ID ...

      - name: Assert the welcome email arrived
        run: |
          curl -s -X POST https://sendtrap.dev/api/v1/assert \
            -H "Authorization: Bearer $SENDTRAP_TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"test_id\": \"$GITHUB_RUN_ID\", \"subject_contains\": \"Welcome\", \"timeout\": 15}" \
            | jq -e '.matched'

      - name: Clear the inbox
        if: always()
        run: |
          curl -s -X DELETE https://sendtrap.dev/api/v1/messages \
            -H "Authorization: Bearer $SENDTRAP_TOKEN"

Parallel runs

Give each environment (staging, PR previews, per-branch) its own inbox and its own token so runs can't see each other's mail. Where an inbox per run isn't practical, a unique test_id per run does the same job inside one shared inbox — every filter and assertion above scopes to it. Keep concurrent blocking calls modest: wait/assert has a tighter rate limit (15/min per token) than ordinary API calls.

The CI container

Prefer a sandbox that lives and dies with the job? The self-hostable Community edition's image has an ephemeral, CI-oriented profile (SENDTRAP_MODE=ci): one docker run starts a throwaway Sendtrap inside your pipeline — SMTP endpoint and API pre-provisioned, no external services, no account, nothing to clean up because all state lives on a tmpfs and vanishes with the job.

bash
docker run --rm \
  -e SENDTRAP_MODE=ci -e APP_URL=http://localhost:8080 \
  --tmpfs /data --tmpfs /tmp --tmpfs /run:rw,exec,nosuid,nodev,mode=755 \
  --tmpfs /app/bootstrap/cache --tmpfs /app/storage/framework --tmpfs /app/storage/logs \
  --read-only \
  --cap-drop ALL \
  --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add FOWNER --cap-add KILL \
  --security-opt no-new-privileges:true \
  -p 8080:8080 -p 1025:1025 \
  ghcr.io/sendtraphq/sendtrap-community:latest

The container boots /up-ready in a few seconds with a project and inbox already seeded on well-known default credentials (SMTP ci / ci-smtp-password, API token ci-api-token) — zero discovery, but never expose :1025/:8080 beyond an isolated CI network. Ready-to-copy GitHub Actions and GitLab CI job files, a compose file, and the full flag-set rationale live in docker/README.md.

The API is the same one documented in the API reference, so tests written against Cloud point at the container with a base URL and token swap — nothing else changes. The durable image for long-lived self-hosted deployments is covered in Self-hosting.

Ready to make CI catch bad email?

Create a free inbox, add its token as a CI secret, and your first email assertion is one curl away.

Create your inbox

No credit card required