package stripe import ( "encoding/json" "errors" "fmt" "io" "log" mrand "math/rand" "net/http" "net/url" "strings" "gpt-plus/pkg/httpclient" ) // randomTimeOnPage returns a random time_on_page value between 15000-45000ms. // A constant value (e.g. "30000") is a fingerprint signal for bot detection. func randomTimeOnPage() string { return fmt.Sprintf("%d", mrand.Intn(30001)+15000) } // localeForCountry returns a browser locale based on billing country code. func localeForCountry(country string) string { switch country { case "GB": return "en-GB" case "DE", "AT", "CH": return "de-DE" case "FR": return "fr-FR" case "JP": return "ja-JP" case "CN": return "zh-CN" default: return "en-US" } } // timezoneForCountry returns a browser timezone based on billing country code. func timezoneForCountry(country string) string { switch country { case "GB": return "Europe/London" case "DE", "AT", "CH": return "Europe/Berlin" case "FR": return "Europe/Paris" case "JP": return "Asia/Tokyo" case "CN": return "Asia/Shanghai" default: return "America/Chicago" } } // ErrCardDeclined indicates the card was declined by Stripe. var ErrCardDeclined = errors.New("card declined") const ( // DefaultLanguage is the browser locale for US cards. DefaultLanguage = "en-US" // DefaultAcceptLanguage is the Accept-Language header for US cards. DefaultAcceptLanguage = "en-US,en;q=0.9" chromeSecChUa = `"Google Chrome";v="145", "Chromium";v="145", "Not-A.Brand";v="99"` stripeOrigin = "https://js.stripe.com" ) // CheckoutResult holds the response from creating a checkout session. type CheckoutResult struct { CheckoutSessionID string PublishableKey string ClientSecret string ExpectedAmount int ProcessorEntity string } // CreateCheckoutSession creates a Stripe checkout session via ChatGPT API. func CreateCheckoutSession(client *httpclient.Client, accessToken, deviceID, sentinelToken string, body map[string]interface{}) (*CheckoutResult, error) { jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal checkout body: %w", err) } req, err := http.NewRequest("POST", "https://chatgpt.com/backend-api/payments/checkout", strings.NewReader(string(jsonBody))) if err != nil { return nil, fmt.Errorf("create checkout request: %w", err) } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Content-Type", "application/json") req.Header.Set("Origin", "https://chatgpt.com") req.Header.Set("Referer", "https://chatgpt.com/") req.Header.Set("Oai-Device-Id", deviceID) req.Header.Set("Openai-Sentinel-Token", sentinelToken) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("send checkout request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read checkout response: %w", err) } log.Printf("[stripe] checkout response (status=%d): %s", resp.StatusCode, string(respBody)) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("checkout failed: status %d, body: %s", resp.StatusCode, string(respBody)) } var result map[string]interface{} if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("parse checkout response: %w", err) } cr := &CheckoutResult{ ProcessorEntity: "openai_llc", } if v, ok := result["checkout_session_id"].(string); ok { cr.CheckoutSessionID = v } else if v, ok := result["session_id"].(string); ok { cr.CheckoutSessionID = v } if v, ok := result["publishable_key"].(string); ok { cr.PublishableKey = v } else if v, ok := result["stripe_publishable_key"].(string); ok { cr.PublishableKey = v } if v, ok := result["client_secret"].(string); ok { cr.ClientSecret = v } if v, ok := result["amount_total"].(float64); ok { cr.ExpectedAmount = int(v) } else if v, ok := result["amount"].(float64); ok { cr.ExpectedAmount = int(v) } if v, ok := result["processor_entity"].(string); ok { cr.ProcessorEntity = v } return cr, nil } // InitResult holds the response from init checkout session. type InitResult struct { InitChecksum string } // stripeVersionWithBetas returns the _stripe_version value with checkout betas appended. func stripeVersionWithBetas(base string) string { return base + "; checkout_server_update_beta=v1; checkout_manual_approval_preview=v1" } // InitCheckoutSession calls POST /v1/payment_pages/{cs_id}/init to get init_checksum. func InitCheckoutSession(client *httpclient.Client, csID, publishableKey, stripeVersion, stripeJsID, userAgent, billingCountry string) (*InitResult, error) { initURL := fmt.Sprintf("https://api.stripe.com/v1/payment_pages/%s/init", csID) form := url.Values{} form.Set("browser_locale", localeForCountry(billingCountry)) form.Set("browser_timezone", timezoneForCountry(billingCountry)) form.Set("elements_session_client[client_betas][0]", "custom_checkout_server_updates_1") form.Set("elements_session_client[client_betas][1]", "custom_checkout_manual_approval_1") form.Set("elements_session_client[elements_init_source]", "custom_checkout") form.Set("elements_session_client[referrer_host]", "chatgpt.com") form.Set("elements_session_client[stripe_js_id]", stripeJsID) form.Set("elements_session_client[locale]", localeForCountry(billingCountry)) form.Set("elements_session_client[is_aggregation_expected]", "false") form.Set("key", publishableKey) form.Set("_stripe_version", stripeVersionWithBetas(stripeVersion)) req, err := http.NewRequest("POST", initURL, strings.NewReader(form.Encode())) if err != nil { return nil, fmt.Errorf("create init request: %w", err) } setStripeHeaders(req, userAgent) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("send init request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read init response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("init failed: status %d, body: %s", resp.StatusCode, string(respBody)) } var result map[string]interface{} if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("parse init response: %w", err) } ir := &InitResult{} if v, ok := result["init_checksum"].(string); ok { ir.InitChecksum = v } // Log amount fields from init for debugging log.Printf("[stripe] init response: amount_total=%v, amount=%v", result["amount_total"], result["amount"]) return ir, nil } // UpdateCheckoutParams holds parameters for the intermediate checkout update. type UpdateCheckoutParams struct { CheckoutSessionID string PublishableKey string StripeVersion string // API version e.g. "2025-03-31.basil" StripeJsID string BillingCountry string BillingAddress string BillingCity string BillingState string BillingZip string UserAgent string } // UpdateResult holds the response from updating a checkout session. type UpdateResult struct { UpdatedAmount int // amount_total after tax recalculation } // UpdateCheckoutSession sends an intermediate POST to /v1/payment_pages/{cs_id} // to update tax_region info. This must be called before confirm to accept terms. func UpdateCheckoutSession(client *httpclient.Client, params *UpdateCheckoutParams) (*UpdateResult, error) { updateURL := fmt.Sprintf("https://api.stripe.com/v1/payment_pages/%s", params.CheckoutSessionID) form := url.Values{} form.Set("tax_region[country]", params.BillingCountry) if params.BillingAddress != "" { form.Set("tax_region[line1]", params.BillingAddress) } if params.BillingCity != "" { form.Set("tax_region[city]", params.BillingCity) } if params.BillingState != "" { form.Set("tax_region[state]", params.BillingState) } if params.BillingZip != "" { form.Set("tax_region[postal_code]", params.BillingZip) } form.Set("elements_session_client[client_betas][0]", "custom_checkout_server_updates_1") form.Set("elements_session_client[client_betas][1]", "custom_checkout_manual_approval_1") form.Set("elements_session_client[elements_init_source]", "custom_checkout") form.Set("elements_session_client[referrer_host]", "chatgpt.com") form.Set("elements_session_client[stripe_js_id]", params.StripeJsID) form.Set("elements_session_client[locale]", localeForCountry(params.BillingCountry)) form.Set("elements_session_client[is_aggregation_expected]", "false") form.Set("client_attribution_metadata[merchant_integration_additional_elements][0]", "payment") form.Set("client_attribution_metadata[merchant_integration_additional_elements][1]", "address") form.Set("key", params.PublishableKey) form.Set("_stripe_version", stripeVersionWithBetas(params.StripeVersion)) req, err := http.NewRequest("POST", updateURL, strings.NewReader(form.Encode())) if err != nil { return nil, fmt.Errorf("create update request: %w", err) } setStripeHeaders(req, params.UserAgent) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("send update request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read update response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("update checkout failed: status %d, body: %s", resp.StatusCode, string(respBody)) } var result map[string]interface{} if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("parse update response: %w", err) } ur := &UpdateResult{} // Try top-level amount fields first if v, ok := result["amount_total"].(float64); ok { ur.UpdatedAmount = int(v) } else if v, ok := result["amount"].(float64); ok { ur.UpdatedAmount = int(v) } // Try total_summary.due (Stripe checkout session structure: {"due":2000,"subtotal":2000,"total":2000}) if ur.UpdatedAmount == 0 { if ts, ok := result["total_summary"].(map[string]interface{}); ok { if v, ok := ts["due"].(float64); ok { ur.UpdatedAmount = int(v) } else if v, ok := ts["total"].(float64); ok { ur.UpdatedAmount = int(v) } } } log.Printf("[stripe] update resolved amount=%d", ur.UpdatedAmount) return ur, nil } // PaymentMethodParams holds parameters for creating a payment method. type PaymentMethodParams struct { CardNumber string CardCVC string CardExpMonth string CardExpYear string BillingName string BillingEmail string BillingCountry string BillingAddress string BillingCity string BillingState string BillingZip string GUID string MUID string SID string PublishableKey string BuildHash string // e.g. "ede17ac9fd" — used in payment_user_agent StripeVersion string // e.g. "2025-03-31.basil" — used in _stripe_version UserAgent string } // PaymentMethodResult holds the response from creating a payment method. type PaymentMethodResult struct { PaymentMethodID string } // CreatePaymentMethod calls POST /v1/payment_methods to create a pm_xxx. func CreatePaymentMethod(client *httpclient.Client, params *PaymentMethodParams) (*PaymentMethodResult, error) { form := url.Values{} form.Set("type", "card") form.Set("card[number]", params.CardNumber) form.Set("card[cvc]", params.CardCVC) form.Set("card[exp_month]", params.CardExpMonth) form.Set("card[exp_year]", params.CardExpYear) form.Set("billing_details[name]", params.BillingName) form.Set("billing_details[email]", params.BillingEmail) form.Set("billing_details[address][country]", params.BillingCountry) if params.BillingAddress != "" { form.Set("billing_details[address][line1]", params.BillingAddress) } if params.BillingCity != "" { form.Set("billing_details[address][city]", params.BillingCity) } if params.BillingState != "" { form.Set("billing_details[address][state]", params.BillingState) } if params.BillingZip != "" { form.Set("billing_details[address][postal_code]", params.BillingZip) } form.Set("allow_redisplay", "unspecified") form.Set("pasted_fields", "number,exp,cvc") form.Set("payment_user_agent", fmt.Sprintf("stripe.js/%s; stripe-js-v3/%s; payment-element; deferred-intent", params.BuildHash, params.BuildHash)) form.Set("referrer", "https://chatgpt.com") form.Set("time_on_page", randomTimeOnPage()) form.Set("guid", params.GUID) form.Set("muid", params.MUID) form.Set("sid", params.SID) form.Set("key", params.PublishableKey) if params.StripeVersion != "" { form.Set("_stripe_version", stripeVersionWithBetas(params.StripeVersion)) } req, err := http.NewRequest("POST", "https://api.stripe.com/v1/payment_methods", strings.NewReader(form.Encode())) if err != nil { return nil, fmt.Errorf("create payment method request: %w", err) } setStripeHeaders(req, params.UserAgent) req.AddCookie(&http.Cookie{Name: "__stripe_mid", Value: params.MUID}) req.AddCookie(&http.Cookie{Name: "__stripe_sid", Value: params.SID}) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("send payment method request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read payment method response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("create payment method failed: status %d, body: %s", resp.StatusCode, string(respBody)) } var result map[string]interface{} if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("parse payment method response: %w", err) } pmr := &PaymentMethodResult{} if v, ok := result["id"].(string); ok { pmr.PaymentMethodID = v } if pmr.PaymentMethodID == "" { return nil, fmt.Errorf("no payment method ID in response: %s", string(respBody)) } return pmr, nil } // ConfirmParams holds all parameters needed to confirm a Stripe payment. type ConfirmParams struct { CheckoutSessionID string PaymentMethodID string InitChecksum string StripeJsID string GUID string MUID string SID string ExpectedAmount int PublishableKey string StripeVersion string // build hash, e.g. "ede17ac9fd" StripeAPIVersion string // API version, e.g. "2025-03-31.basil" UserAgent string } // DirectConfirmParams holds parameters for confirm with inline card data (no tokenization). type DirectConfirmParams struct { CheckoutSessionID string // Card details (inline) CardNumber string CardCVC string CardExpMonth string CardExpYear string // Billing BillingName string BillingEmail string BillingCountry string BillingAddress string BillingCity string BillingState string BillingZip string // Fingerprint GUID string MUID string SID string // Stripe ExpectedAmount int InitChecksum string // from InitCheckoutSession PublishableKey string BuildHash string // e.g. "ede17ac9fd" StripeVersion string // API version, e.g. "2025-03-31.basil" StripeJsID string // per-session UUID, like stripe.js generates UserAgent string PassiveCaptchaToken string // passive hCaptcha token from telemetry PPageID string // payment page ID for telemetry } // ConfirmResult holds the parsed confirm response. type ConfirmResult struct { // RequiresAction is true when setup_intent needs challenge verification. RequiresAction bool // Challenge fields (only set when RequiresAction is true) SetupIntentID string // seti_xxx ClientSecret string // seti_xxx_secret_xxx SiteKey string // hCaptcha site_key from stripe_js RqData string // hCaptcha rqdata from stripe_js VerificationURL string // /v1/setup_intents/{seti}/verify_challenge // ReturnURL is the merchant-configured return URL from the confirm response. // For Team plans, it contains account_id (workspace ID) as a query parameter. ReturnURL string } // ConfirmPayment confirms payment directly with Stripe using pm_xxx. func ConfirmPayment(client *httpclient.Client, params *ConfirmParams) (*ConfirmResult, error) { confirmURL := fmt.Sprintf("https://api.stripe.com/v1/payment_pages/%s/confirm", params.CheckoutSessionID) returnURL := fmt.Sprintf( "https://checkout.stripe.com/c/pay/%s?returned_from_redirect=true", params.CheckoutSessionID, ) form := url.Values{} form.Set("guid", params.GUID) form.Set("muid", params.MUID) form.Set("sid", params.SID) form.Set("payment_method", params.PaymentMethodID) if params.InitChecksum != "" { form.Set("init_checksum", params.InitChecksum) } form.Set("version", params.StripeVersion) form.Set("expected_amount", fmt.Sprintf("%d", params.ExpectedAmount)) form.Set("expected_payment_method_type", "card") form.Set("return_url", returnURL) form.Set("elements_session_client[client_betas][0]", "custom_checkout_server_updates_1") form.Set("elements_session_client[client_betas][1]", "custom_checkout_manual_approval_1") form.Set("elements_session_client[elements_init_source]", "custom_checkout") form.Set("elements_session_client[referrer_host]", "chatgpt.com") form.Set("elements_session_client[stripe_js_id]", params.StripeJsID) form.Set("elements_session_client[locale]", "en-US") form.Set("elements_session_client[is_aggregation_expected]", "false") form.Set("client_attribution_metadata[merchant_integration_source]", "checkout") form.Set("client_attribution_metadata[merchant_integration_version]", "custom") form.Set("client_attribution_metadata[merchant_integration_subtype]", "payment-element") form.Set("client_attribution_metadata[merchant_integration_additional_elements][0]", "payment") form.Set("client_attribution_metadata[merchant_integration_additional_elements][1]", "address") form.Set("client_attribution_metadata[payment_intent_creation_flow]", "deferred") form.Set("client_attribution_metadata[payment_method_selection_flow]", "automatic") form.Set("consent[terms_of_service]", "accepted") form.Set("key", params.PublishableKey) form.Set("_stripe_version", stripeVersionWithBetas(params.StripeAPIVersion)) req, err := http.NewRequest("POST", confirmURL, strings.NewReader(form.Encode())) if err != nil { return nil, fmt.Errorf("create confirm request: %w", err) } setStripeHeaders(req, params.UserAgent) req.AddCookie(&http.Cookie{Name: "__stripe_mid", Value: params.MUID}) req.AddCookie(&http.Cookie{Name: "__stripe_sid", Value: params.SID}) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("send confirm request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read confirm response: %w", err) } log.Printf("[stripe] confirm response (status=%d): %s", resp.StatusCode, string(respBody)) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("confirm failed: status %d, body: %s", resp.StatusCode, string(respBody)) } return parseConfirmResponse(respBody) } // parseConfirmResponse extracts challenge info from the confirm JSON response. func parseConfirmResponse(body []byte) (*ConfirmResult, error) { var raw map[string]interface{} if err := json.Unmarshal(body, &raw); err != nil { return nil, fmt.Errorf("parse confirm response: %w", err) } cr := &ConfirmResult{} // Extract return_url — for Team plans this contains the workspace account_id if v, ok := raw["return_url"].(string); ok { cr.ReturnURL = v log.Printf("[stripe] confirm return_url: %s", v) } // Check setup_intent.status si, ok := raw["setup_intent"].(map[string]interface{}) if !ok { // No setup_intent — payment may have succeeded directly return cr, nil } status, _ := si["status"].(string) // Check for setup errors that indicate card decline (200 response but failed state) if lastErr, ok := si["last_setup_error"].(map[string]interface{}); ok { errCode, _ := lastErr["code"].(string) errMsg, _ := lastErr["message"].(string) errType, _ := lastErr["type"].(string) if errCode != "" || errType == "card_error" { return nil, fmt.Errorf("setup_intent error: code=%s, type=%s, message=%s", errCode, errType, errMsg) } } // Check for requires_payment_method (card was rejected, need new card) if status == "requires_payment_method" { return nil, fmt.Errorf("setup_intent requires new payment method (card rejected)") } if status != "requires_action" { // succeeded or other non-challenge status return cr, nil } cr.RequiresAction = true cr.SetupIntentID, _ = si["id"].(string) cr.ClientSecret, _ = si["client_secret"].(string) // Extract challenge details from next_action.use_stripe_sdk.stripe_js nextAction, _ := si["next_action"].(map[string]interface{}) if nextAction == nil { return cr, nil } useSDK, _ := nextAction["use_stripe_sdk"].(map[string]interface{}) if useSDK == nil { return cr, nil } // stripe_js contains site_key, rqdata, verification_url stripeJS, _ := useSDK["stripe_js"].(map[string]interface{}) if stripeJS != nil { cr.SiteKey, _ = stripeJS["site_key"].(string) cr.RqData, _ = stripeJS["rqdata"].(string) cr.VerificationURL, _ = stripeJS["verification_url"].(string) } // Fallback: top-level fields in use_stripe_sdk if cr.SiteKey == "" { cr.SiteKey, _ = useSDK["hcaptcha_site_key"].(string) } if cr.RqData == "" { cr.RqData, _ = useSDK["hcaptcha_rqdata"].(string) } if cr.VerificationURL == "" { cr.VerificationURL, _ = useSDK["verification_url"].(string) } log.Printf("[stripe] confirm requires_action: seti=%s, site_key=%s, has_rqdata=%v", cr.SetupIntentID, cr.SiteKey, cr.RqData != "") return cr, nil } // VerifyChallengeParams holds parameters for the verify_challenge call. type VerifyChallengeParams struct { SetupIntentID string // seti_xxx ClientSecret string // seti_xxx_secret_xxx CaptchaToken string // P1_eyJ... from hCaptcha CaptchaEKey string // E1_... from hCaptcha (respKey) PublishableKey string StripeVersion string // API version e.g. "2025-03-31.basil" UserAgent string MUID string SID string } // VerifyChallenge calls POST /v1/setup_intents/{seti}/verify_challenge to complete the hCaptcha challenge. func VerifyChallenge(client *httpclient.Client, params *VerifyChallengeParams) error { verifyURL := fmt.Sprintf("https://api.stripe.com/v1/setup_intents/%s/verify_challenge", params.SetupIntentID) form := url.Values{} form.Set("challenge_response_token", params.CaptchaToken) if params.CaptchaEKey != "" { form.Set("challenge_response_ekey", params.CaptchaEKey) } form.Set("client_secret", params.ClientSecret) form.Set("captcha_vendor_name", "hcaptcha") form.Set("key", params.PublishableKey) form.Set("_stripe_version", stripeVersionWithBetas(params.StripeVersion)) req, err := http.NewRequest("POST", verifyURL, strings.NewReader(form.Encode())) if err != nil { return fmt.Errorf("create verify challenge request: %w", err) } setStripeHeaders(req, params.UserAgent) req.AddCookie(&http.Cookie{Name: "__stripe_mid", Value: params.MUID}) req.AddCookie(&http.Cookie{Name: "__stripe_sid", Value: params.SID}) resp, err := client.Do(req) if err != nil { return fmt.Errorf("send verify challenge: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("read verify challenge response: %w", err) } log.Printf("[stripe] verify_challenge response (status=%d): %s", resp.StatusCode, string(respBody)) if resp.StatusCode != http.StatusOK { return fmt.Errorf("verify challenge failed: status %d, body: %s", resp.StatusCode, string(respBody)) } // Check that setup_intent status is now succeeded var result map[string]interface{} if err := json.Unmarshal(respBody, &result); err != nil { return fmt.Errorf("parse verify challenge response: %w", err) } status, _ := result["status"].(string) if status == "succeeded" { log.Printf("[stripe] verify_challenge succeeded") return nil } // Check for error lastErr, _ := result["last_setup_error"].(map[string]interface{}) if lastErr != nil { msg, _ := lastErr["message"].(string) code, _ := lastErr["code"].(string) return fmt.Errorf("verify challenge error: code=%s, message=%s", code, msg) } return fmt.Errorf("verify challenge unexpected status: %s", status) } // PaymentPagePollParams holds the query parameters for GET /v1/payment_pages/{cs_id}/poll. type PaymentPagePollParams struct { CheckoutSessionID string PublishableKey string StripeVersion string // API version e.g. "2025-03-31.basil" UserAgent string } // PaymentPagePollResult captures the fields needed to determine whether Stripe has fully finalized checkout. type PaymentPagePollResult struct { State string PaymentObjectStatus string ReturnURL string } // PollPaymentPage fetches the current payment page state after confirm/verify_challenge. func PollPaymentPage(client *httpclient.Client, params *PaymentPagePollParams) (*PaymentPagePollResult, error) { q := url.Values{} q.Set("key", params.PublishableKey) q.Set("_stripe_version", stripeVersionWithBetas(params.StripeVersion)) pollURL := fmt.Sprintf( "https://api.stripe.com/v1/payment_pages/%s/poll?%s", params.CheckoutSessionID, q.Encode(), ) req, err := http.NewRequest("GET", pollURL, nil) if err != nil { return nil, fmt.Errorf("create poll request: %w", err) } setStripeHeaders(req, params.UserAgent) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("send poll request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read poll response: %w", err) } log.Printf("[stripe] payment page poll response (status=%d): %s", resp.StatusCode, string(respBody)) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("payment page poll failed: status %d, body: %s", resp.StatusCode, string(respBody)) } var raw map[string]interface{} if err := json.Unmarshal(respBody, &raw); err != nil { return nil, fmt.Errorf("parse poll response: %w", err) } result := &PaymentPagePollResult{} result.State, _ = raw["state"].(string) result.PaymentObjectStatus, _ = raw["payment_object_status"].(string) result.ReturnURL, _ = raw["return_url"].(string) return result, nil } // ConfirmPaymentDirect confirms payment with inline card data — no init or payment_methods step needed. // Flow: checkout → fingerprint(m.stripe.com/6) → confirm(inline card). func ConfirmPaymentDirect(client *httpclient.Client, params *DirectConfirmParams) (*ConfirmResult, error) { confirmURL := fmt.Sprintf("https://api.stripe.com/v1/payment_pages/%s/confirm", params.CheckoutSessionID) returnURL := fmt.Sprintf( "https://checkout.stripe.com/c/pay/%s?returned_from_redirect=true", params.CheckoutSessionID, ) form := url.Values{} // Inline card data form.Set("payment_method_data[type]", "card") form.Set("payment_method_data[card][number]", params.CardNumber) form.Set("payment_method_data[card][cvc]", params.CardCVC) form.Set("payment_method_data[card][exp_month]", params.CardExpMonth) form.Set("payment_method_data[card][exp_year]", params.CardExpYear) form.Set("payment_method_data[billing_details][name]", params.BillingName) if params.BillingEmail != "" { form.Set("payment_method_data[billing_details][email]", params.BillingEmail) } form.Set("payment_method_data[billing_details][address][country]", params.BillingCountry) if params.BillingAddress != "" { form.Set("payment_method_data[billing_details][address][line1]", params.BillingAddress) } if params.BillingCity != "" { form.Set("payment_method_data[billing_details][address][city]", params.BillingCity) } if params.BillingState != "" { form.Set("payment_method_data[billing_details][address][state]", params.BillingState) } if params.BillingZip != "" { form.Set("payment_method_data[billing_details][address][postal_code]", params.BillingZip) } form.Set("payment_method_data[allow_redisplay]", "unspecified") form.Set("payment_method_data[pasted_fields]", "number,exp,cvc") form.Set("payment_method_data[payment_user_agent]", fmt.Sprintf("stripe.js/%s; stripe-js-v3/%s; payment-element; deferred-intent", params.BuildHash, params.BuildHash)) form.Set("payment_method_data[referrer]", "https://chatgpt.com") form.Set("payment_method_data[time_on_page]", randomTimeOnPage()) // Fingerprint form.Set("payment_method_data[guid]", params.GUID) form.Set("payment_method_data[muid]", params.MUID) form.Set("payment_method_data[sid]", params.SID) form.Set("guid", params.GUID) form.Set("muid", params.MUID) form.Set("sid", params.SID) // Build hash version — required by Stripe, was missing from direct confirm form.Set("version", params.BuildHash) if params.InitChecksum != "" { form.Set("init_checksum", params.InitChecksum) } // Payment details form.Set("expected_amount", fmt.Sprintf("%d", params.ExpectedAmount)) form.Set("expected_payment_method_type", "card") form.Set("return_url", returnURL) // Elements session client — required for Stripe risk assessment, was missing from direct confirm form.Set("elements_session_client[client_betas][0]", "custom_checkout_server_updates_1") form.Set("elements_session_client[client_betas][1]", "custom_checkout_manual_approval_1") form.Set("elements_session_client[elements_init_source]", "custom_checkout") form.Set("elements_session_client[referrer_host]", "chatgpt.com") form.Set("elements_session_client[stripe_js_id]", params.StripeJsID) form.Set("elements_session_client[locale]", localeForCountry(params.BillingCountry)) form.Set("elements_session_client[is_aggregation_expected]", "false") // Client attribution metadata — required for Stripe risk assessment, was missing from direct confirm form.Set("client_attribution_metadata[merchant_integration_source]", "checkout") form.Set("client_attribution_metadata[merchant_integration_version]", "custom") form.Set("client_attribution_metadata[merchant_integration_subtype]", "payment-element") form.Set("client_attribution_metadata[merchant_integration_additional_elements][0]", "payment") form.Set("client_attribution_metadata[merchant_integration_additional_elements][1]", "address") form.Set("client_attribution_metadata[payment_intent_creation_flow]", "deferred") form.Set("client_attribution_metadata[payment_method_selection_flow]", "automatic") // js_checksum and rv_timestamp (Stripe.js anti-bot fields) ppageID := params.PPageID if ppageID == "" { ppageID = params.CheckoutSessionID } form.Set("js_checksum", JsChecksum(ppageID)) form.Set("rv_timestamp", RvTimestamp()) // Passive captcha token (from telemetry) if params.PassiveCaptchaToken != "" { form.Set("passive_captcha[passive_token]", params.PassiveCaptchaToken) form.Set("passive_captcha[vendor]", "hcaptcha") } // Consent & auth form.Set("consent[terms_of_service]", "accepted") form.Set("key", params.PublishableKey) form.Set("_stripe_version", stripeVersionWithBetas(params.StripeVersion)) req, err := http.NewRequest("POST", confirmURL, strings.NewReader(form.Encode())) if err != nil { return nil, fmt.Errorf("create direct confirm request: %w", err) } setStripeHeaders(req, params.UserAgent) req.AddCookie(&http.Cookie{Name: "__stripe_mid", Value: params.MUID}) req.AddCookie(&http.Cookie{Name: "__stripe_sid", Value: params.SID}) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("send direct confirm request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read direct confirm response: %w", err) } log.Printf("[stripe] direct confirm response (status=%d): %s", resp.StatusCode, string(respBody)) if resp.StatusCode != http.StatusOK { if strings.Contains(string(respBody), "card_declined") { return nil, fmt.Errorf("confirm payment: %w", ErrCardDeclined) } return nil, fmt.Errorf("direct confirm failed: status %d, body: %s", resp.StatusCode, string(respBody)) } return parseConfirmResponse(respBody) } // setStripeHeaders sets common headers for Stripe API requests. func setStripeHeaders(req *http.Request, userAgent string) { req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", stripeOrigin) req.Header.Set("Referer", stripeOrigin+"/") if userAgent != "" { req.Header.Set("User-Agent", userAgent) } req.Header.Set("Sec-Fetch-Dest", "empty") req.Header.Set("Sec-Fetch-Mode", "cors") req.Header.Set("Sec-Fetch-Site", "cross-site") req.Header.Set("Sec-Ch-Ua", chromeSecChUa) req.Header.Set("Sec-Ch-Ua-Mobile", "?0") req.Header.Set("Sec-Ch-Ua-Platform", `"Windows"`) req.Header.Set("Accept-Language", "en-US,en;q=0.9") }