Subscriptions

Testing

Verify subscription integrations against Throttle's hosted sandbox surfaces and your own staging storefront. Treat Throttle as an external API: configure keys, origins, webhooks, and assertions the same way you will in production.

Use sandbox credentials only
Keep secret keys on your backend, use a non-production workspace/application for test runs, and never put a secret key in browser code. Browser code should only receive session identifiers and embed data returned by your backend.

Sandbox checklist

Before exercising a subscription flow, confirm the environment pieces below. The exact values come from your Throttle dashboard or onboarding handoff.

ItemWhere it belongsWhat to verify
Secret API keyMerchant backendRequests to Throttle are made server-side with a non-production environment key.
Application idMerchant backendCheckout-session requests target the sandbox application you intend to test.
Allowed originThrottle dashboardYour staging storefront origin is allowlisted before mounting the iframe.
Webhook endpointMerchant backendThe endpoint is reachable by Throttle and verifies the webhook signature.
Return and cancel URLsCheckout-session requestBoth URLs point back to your staging storefront and handle terminal states.
Test payment methodHosted checkout iframeUse the sandbox card or payment method values provided for your account.
Backend configuration
THROTTLE_API_URL=https://api.usethrottle.dev
THROTTLE_CHECKOUT_ORIGIN=https://checkout.usethrottle.dev
THROTTLE_SECRET_KEY=sk_uat_...
THROTTLE_APPLICATION_ID=e7efb0a6-892e-46b2-97ab-296bb04c5b29
THROTTLE_WEBHOOK_SECRET=whsec_...

Manual smoke test

A passing smoke test proves that your backend can create the session, the browser can mount the hosted payment surface, and your webhook endpoint can process durable subscription events.

  1. Open your staging pricing or subscribe page.
  2. Select a plan that creates a recurring checkout session.
  3. Confirm your backend creates the session with a recurring block.
  4. Mount the Throttle payment or checkout embed from the returned session.
  5. Complete the payment with sandbox payment details for your account.
  6. Confirm your storefront receives the success callback and renders the subscription confirmation state.
  7. Confirm your webhook endpoint receives subscription.created and any payment events needed by your ledger.
Session request
curl -X POST https://api.usethrottle.dev/api/v1/checkout/sessions \
  -H "x-api-key: sk_uat_..." \
  -H "content-type: application/json" \
  -d '{
    "applicationId": "e7efb0a6-892e-46b2-97ab-296bb04c5b29",
    "externalCartId": "sub-test-001",
    "returnUrl": "https://staging.shop.example.com/account",
    "cancelUrl": "https://staging.shop.example.com/pricing",
    "customer": {
      "externalCustomerId": "user_test_001",
      "email": "buyer@example.com"
    },
    "recurring": {
      "plan": "pro_monthly",
      "planName": "Pro Monthly",
      "interval": "monthly",
      "amount": 2999,
      "trialDays": 14
    },
    "metadata": {
      "testRunId": "sub-flow-2026-05-05"
    }
  }'

Trial-abuse replay

To validate trial-abuse protection, repeat the subscription flow with the same physical card fingerprint and a different customer identity. The expected result is a subscription without the requested trial and a subscription.trial_blocked webhook.

StepExpected result
First subscriptionSubscription is created with the requested trial window.
Second subscription with same cardSubscription is created without the trial window.
Webhook assertionReceive subscription.trial_blocked with a reason such as card_already_used_for_trial.
Customer experienceYour storefront should explain that the buyer is subscribed without a new trial.

Webhook assertions

Subscription state is durable only after your webhook handler processes signed events idempotently. Store event ids before applying side effects and treat delivery as at-least-once.

Handler shape
import { verifyThrottleWebhook } from '@/lib/throttle/webhooks';

export async function POST(req: Request) {
  const rawBody = await req.text();
  const signature = req.headers.get('x-throttle-signature');
  if (!signature || !verifyThrottleWebhook(rawBody, signature, process.env.THROTTLE_WEBHOOK_SECRET!)) {
    return new Response('Invalid signature', { status: 400 });
  }

  const event = JSON.parse(rawBody);

  if (await db.webhookEvents.exists(event.id)) {
    return Response.json({ ok: true });
  }

  await db.webhookEvents.insert({
    id: event.id,
    type: event.type,
    receivedAt: new Date(),
  });

  if (event.type === 'subscription.created') {
    await provisionAccess(event.data.subscription);
  }

  if (event.type === 'subscription.cancelled') {
    await revokeAccess(event.data.subscription);
  }

  return Response.json({ ok: true });
}
ScenarioEvents to assert
Subscription createdsubscription.created plus the payment event your ledger expects.
Trial endssubscription.activated
Renewal succeedssubscription.renewed
Renewal failssubscription.payment_failed, then subscription.past_due when the threshold is crossed.
Cancellationsubscription.cancelled with the expected data.reason.

Automated test boundaries

Automated browser tests should validate your application's behavior around the embed: session creation, iframe mounting, event listeners, success routing, and webhook-driven state updates. They should not reach into the hosted payment iframe or depend on private Throttle implementation details.

LayerTest thisAvoid this
Unit testsYour backend request builder, webhook signature verification, and idempotency.Depending on undocumented Throttle behavior.
Browser testsYour pricing page, terms gates, iframe container, postMessage handlers, and success state.Clicking card fields inside the hosted payment iframe.
Integration smoke testsA sandbox checkout from session creation through webhook side effects.Assuming delivery order for webhook events.

Common gotchas

  • Origin not allowlisted. The iframe refuses to render until the parent storefront origin is configured for the merchant.
  • Webhook signature mismatch. Verify against the webhook secret for the same merchant environment that sent the event.
  • Duplicate webhook side effects. Throttle retries delivery; store event ids and make every handler idempotent.
  • Expired checkout session. Create a fresh session for every buyer attempt instead of reusing old session ids.
  • Testing inside the iframe. The payment surface is hosted and isolated. Assert the events it emits and the state your app persists.

Next