2025-12-20 | PreviewProof Team

Stripe, Twilio, and SendGrid Test Mode in Preview Environments

StripeTwilioSendGridpreview environmentstest modethird-party integrations

Stripe, Twilio, and SendGrid are the three most common third-party integrations in modern web apps, and all three offer official test modes. On paper, previewing code that touches them is straightforward — test creds in previews, production creds in production, done. In practice, each one has a specific gotcha that bites teams the first time they wire it up at preview-environment scale.

Stripe: Test Mode Is Easy. Webhook Routing Isn’t.

Stripe’s test mode is well-designed. Test API keys (sk_test_...) hit the same endpoints, return realistic responses, never charge a real card. Test card numbers (4242 4242 4242 4242 and friends) trigger specific scenarios. You can build the entire payment flow against test mode with high confidence.

The gotcha is webhooks. Test mode generates real webhook events — payment_intent.succeeded, customer.subscription.updated — and they need to land somewhere reachable. In a preview, “reachable” means the dynamic preview URL Stripe doesn’t know about.

The full catalog is in webhook handling when your preview URL changes. For Stripe specifically: you get 16 endpoints per account in test mode (workable for small fleets via API register/deregister), or route through a stable proxy and dispatch on metadata.preview_id set at intent creation. stripe listen --forward-to is fine locally, useless unattended.

The other thing that catches teams: Stripe’s test mode has its own data namespace. Customers and subscriptions in test mode are invisible to production. If your seed script needs Stripe customer IDs, those IDs need to exist in test mode. Bootstrap a fixed set of test customers idempotently:

const stripeCustomerIds = {
alice: 'cus_TestAlice123',
bob: 'cus_TestBob456',
};
await prisma.user.create({
data: { email: '[email protected]', stripeCustomerId: stripeCustomerIds.alice },
});

Twilio: Test Credentials Don’t Actually Send Messages

Twilio’s test credentials look identical to real credentials — they accept API calls, return success — and do nothing visible. The SMS isn’t sent. The voice call doesn’t happen. There’s no inbox to check.

Sometimes that’s what you want. Sometimes it’s a problem. If a developer is iterating on an SMS template — verifying token interpolation, message length, the From number — they need to see the rendered output.

Pattern A: Capture and display. Wrap your Twilio client. In previews, capture the message and write it to your DB. Surface in a preview-only admin route.

class PreviewTwilioClient {
async sendSms({ to, from, body }: SendSmsArgs) {
if (process.env.PREVIEW_MODE) {
await db.capturedMessages.insert({ provider: 'twilio', channel: 'sms', to, from, body });
return { sid: `preview_${randomId()}` };
}
return realTwilioClient.messages.create({ to, from, body });
}
}

Reviewer hits /admin/messages and sees every SMS that “would have been sent.”

Pattern B: Magic Numbers. Twilio’s magic numbers exercise specific behaviors with test credentials. +15005550006 always succeeds. +15005550001 returns “invalid number.” +15005550009 is unsubscribed. Use for error-path testing; combine with Pattern A for content verification.

Pattern C: Real send to a controlled number. Production credentials but only sends to a hardcoded allowlisted number the team owns. Higher risk, occasionally worth it for end-to-end verification including the carrier path. Lock with code-level allowlists, not config.

The trap: production credentials in previews without an allowlist. One developer seeds a million users with realistic phone numbers, an unguarded notification fires, and you have a very expensive problem.

SendGrid: Sandbox Mode Silently Swallows Mail

SendGrid’s sandbox mode is the most subtle of the three. Set mail_settings.sandbox_mode.enable: true and SendGrid responds with a 200 — without sending. It’s opt-in per request: forget the flag and a real email goes out; remember it and the email silently disappears. Either way, a reviewer verifying “did the welcome email go out, and did it look right” sees nothing useful.

MailHog or Mailpit, deployed alongside the preview. A tiny SMTP server with a web UI. Use a generic mailer abstraction that talks to SendGrid in production and to Mailpit in previews.

services:
app:
environment:
MAIL_TRANSPORT: smtp
SMTP_HOST: mailpit
SMTP_PORT: 1025
mailpit:
image: axllent/mailpit
ports: ["8025:8025"]

The reviewer gets a /mailpit URL alongside the preview, sees every email that would have been sent, including HTML rendering. The right answer for most teams.

The alternative — capture-and-display like Twilio Pattern A — works if you already have an admin UI. Using SendGrid’s actual sandbox mode is fine for “did the API call succeed” and useless for everything else.

Rate Limits and Sandbox Quotas

Sandbox accounts have rate limits, and preview environments hit them faster than expected. Stripe’s test limits are generous but not unlimited. Twilio test credentials have lower limits than production. SendGrid’s free tier caps at 100 emails per day — a busy preview fleet blows through that in an hour if seed data triggers welcome emails.

Two defenses: throttle outbound third-party calls per preview at something modest, and aggregate — a test that creates 50 users doesn’t need 50 webhook events to verify “user creation works.” This connects to cost-aware preview environments — third-party quotas are a cost in disguise.

Don’t Mix Production Credentials Into Previews

The biggest failure mode across all three providers: production credentials accidentally injected into a preview. A copied .env, a misnamed secret, a CI variable scoped to “all environments” instead of “production only.”

Mitigation: environment-class isolation in your secrets manager. Production credentials live in the production scope, unreadable by preview provisioning. Preview-class secrets are separate. The pattern is layer 2 in environment variables for ephemeral previews.

A useful belt-and-suspenders: refuse to start if NODE_ENV !== 'production' and any credential matches a production prefix (sk_live_, real Twilio SIDs, real SendGrid keys). One-line check, saves bad afternoons.

Putting It Together

The cleanest configuration:

  • Stripe: Test API key + webhook routing through a stable proxy + bootstrapped test customers in seed data
  • Twilio: Test credentials + capture-and-display abstraction for verification + magic numbers for error paths
  • SendGrid: Mailpit or equivalent for content verification + production credentials behind a hardcoded allowlist if real-send is needed

Each is a small ongoing investment that solves a specific failure mode. None are exotic.


If you’d rather not run Mailpit, build a Twilio capture abstraction, and wire up Stripe webhook routing for every project, PreviewProof ships with first-class support for the major third-party test modes — captured mail and SMS visible in the preview itself, webhook routing handled, and credentials scoped so production keys never end up in a preview by accident. If you’re building it yourself, the patterns above hold up; the part that’s worth getting right early is making sure reviewers can see the messages, not just trust that they were “sent.”