Stripe International Payments
How I integrated Stripe at Binocs to handle USD, EUR, and INR payments with 3DS, webhooks, and idempotency.
At Binocs I wired Stripe to take payments in USD, EUR, and INR from compliance customers across 14 countries. The core flow is Payment Intents, not the old Charges API, because PaymentIntent handles SCA and 3DS for us automatically.
The 30-second pitch
The browser asks our backend for a PaymentIntent. Backend creates it on Stripe with the amount and currency, returns the client_secret. Stripe.js confirms the payment in the browser, which lets Stripe collect the card without the card ever touching our server. Stripe then fires a payment_intent.succeeded webhook to our backend. We verify the signature, mark the invoice paid in Postgres, and trigger downstream side effects.
We never trust the redirect or the client-side success callback. The webhook is the source of truth.
What I actually got right
- Idempotency keys on every
PaymentIntent.createcall. The key is the invoice ID plus a retry counter. Stripe dedups for 24 hours, so retries on network blips do not double-charge. - Webhook signature verification using the raw request body. FastAPI gives you
request.body()before parsing, you need that exact bytes for the HMAC to match. - Storing the Stripe event ID and rejecting duplicates. Stripe retries webhooks up to 3 days, so the same event will arrive multiple times.
- Currency handled in the smallest unit (cents, paise). Never store amounts as floats. Stripe expects integers and so does our DB.
What broke in production
- INR has a special restriction: cross-border INR charges require export compliance and a registered Indian entity. We hit this on day one. Solution was to route INR through a separate Razorpay flow and keep Stripe for USD/EUR.
- 3DS in EUR triggers on almost every transaction under PSD2 SCA. Our latency budget had to expand to handle the OTP step.
Learn more
- Docs
- DocsStripe Docs: WebhooksStripe
- DocsStripe Docs: IdempotencyStripe