Back to the blog

Fixing Sandbox Checkout: Pricing Conflicts, Redirect Validation, and Fulfillment Permissions

Tracing sandbox checkout failures across adaptive pricing conflicts, localhost redirects, webhook registration, and user-provisioning permissions—with separate checks for each repaired boundary.

Introduction

I changed a sandbox checkout-to-membership flow at four points: plan preparation, hosted-checkout redirects, webhook delivery, and user provisioning. At first, adaptive pricing caused the prepare route to return 409, preventing the checkout page from rendering. Redirect clicks then returned a generic 502 because the request included a non-HTTPS localhost redirect URL. Even after a successful test purchase, no Cognito user or email was generated: the webhook wasn't registered, and the fulfillment function lacked the permissions needed to create users.

I disabled adaptive pricing, restricted redirect URLs, registered the webhook, and added the missing IAM permissions. Checkout rendered, redirects stopped returning that 502, and payment.succeeded events reached the webhook and updated order status in the dashboard. After redeployment, I also verified that one affected account was backfilled and linked. These results confirm separate parts of the repair. They don't establish that every downstream effect, including email delivery, was independently verified.

The Rule That Shipped

The implementation has three explicit configured states. First, adaptive pricing is disabled on the plan. Enabling it in this sandbox configuration caused the prepare-route 409; disabling it allowed checkout to render. This is a plan setting, not a retry policy or an application-side workaround for arbitrary conflicts.

Second, redirect_url is sent only when it is a valid HTTPS URL. If the host is localhost, the field is omitted entirely—even if the candidate URL otherwise appears usable. The rule isn't to forward whatever return address local development provides. Omitting the field is also different from sending an empty value or forwarding the rejected HTTP address.

Third, the fulfillment Lambda's IAM policy includes both cognito-idp:ListUsers and cognito-idp:AdminCreateUser. I redeployed the function after adding those actions. Registering the webhook was another necessary setup correction: permissions can't compensate for an endpoint that hasn't been registered to receive events. Each change addresses a separate prerequisite, rather than a single cause behind all the checkout failures.

Worked Example

I registered the sandbox webhook endpoint with its signing secret, then completed a real test purchase through hosted checkout using the provider's test cards. The sequence included firing payment.succeeded against the registered webhook. The endpoint received the event, and the order status updated accordingly in the dashboard. Receipt and status updates were confirmed across multiple test cards, not inferred from one successful payment screen.

A separate synthetic payment.created test returned 502. It wasn't the event the fulfillment handler expected; payment.succeeded was the event used to trigger fulfillment. That mismatch makes payment.created an unsuitable substitute for the successful worked case, but it doesn't establish the cause of the HTTP failure. An irrelevant event could be acknowledged without provisioning anything. Without supporting logs or documentation, I can't describe the 502 as inherently correct or harmless.

The observed difference is specific: real sandbox purchases and payment.succeeded testing established webhook receipt and dashboard updates, while the synthetic payment.created attempt failed with 502. The underlying reason for that response remains unverified.

Challenges and Tradeoffs

I used hosted checkout because the embeddable alternative was unavailable for the needed flow, and kept a mechanism to switch between the two. That made the sandbox path testable but left two checkout paths to maintain. Success with hosted checkout doesn't establish that the embedded path behaves identically, especially around navigation and completion handling.

The redirect rule has a separate tradeoff. Omitting redirect_url on localhost avoids submitting a value known to be rejected, but it doesn't exercise the HTTPS post-payment return journey locally. A valid HTTPS development endpoint would test a different part of that journey. Omission fixes request acceptance without proving return-page behavior, so I treat checkout navigation and application return handling as separate checks.

Synthetic events are still useful, but what they test depends on the event type and payload. They were insufficient to prove this end-to-end flow on their own, not inherently incapable of exercising fulfillment. Likewise, adding the two IAM actions addresses the documented authorization gap. Permission to create users doesn't by itself establish delivery, event handling, account linking, or email delivery.

Verification Process

I checked each repaired boundary against an observable result. After disabling adaptive pricing and restarting the development server, the checkout page rendered. After applying the HTTPS-only rule and omitting redirect_url on localhost, redirect clicks stopped producing the earlier 502. With the webhook registered, payment.succeeded reached the endpoint and order status updated in the dashboard across multiple test cards.

For provisioning, I added cognito-idp:ListUsers and cognito-idp:AdminCreateUser and redeployed the Lambda. The recorded post-change outcome is that future checkouts automatically create Cognito users. The concrete account-level verification described here is narrower: one affected email was backfilled, and its cognitoSub was correctly linked to the account. I don't have a separately documented fresh post-deployment purchase showing the full payment-to-user-creation-and-linking sequence, so I can't claim that stronger verification. The backfill also doesn't establish successful email delivery.

Three other checks remain open. I haven't established whether other accounts were affected before the permissions were added, whether the signing-secret fallback has been exercised by a real mismatched-secret scenario, or whether disabling adaptive pricing must be repeated when the plan is recreated or cloned. The synthetic payment.created 502 also needs its own diagnostic evidence; successful payment.succeeded handling doesn't explain it.

Conclusion

The repair involved specific configuration and request rules, not a generic response to 409 or 502: disable adaptive pricing on the plan, send redirect_url only for valid HTTPS destinations while omitting it on localhost, register the webhook, and grant the fulfillment function both required Cognito actions.

I observed checkout rendering, corrected redirect behavior, payment.succeeded receipt, dashboard status updates, and a backfilled account linked through cognitoSub. The recorded resulting state includes automatic Cognito creation for future checkouts, but the detailed verification available here stops short of a separately traced fresh provisioning run. That distinction preserves both the reported outcome and the limits of the supporting test record.

The remaining work is specific: investigate the synthetic-event 502, check for other affected accounts, exercise the signing-secret fallback, and establish what happens to the adaptive-pricing setting when a plan is recreated or cloned.