'How to capture payment data from Stripe Payment Element

I've finally managed to implement the new Stripe Payment Element in Laravel via the Payment Intents API. However, I now need to capture information about the payments and store them in my database - specifically, I need the following data:

  • Transaction ID
  • Payment status (failed/pending/successful, etc.)
  • Payment method type (card/Google Pay/Apple Pay/etc.)
  • The amount actually charged to the customer
  • The currency the customer actually paid in
  • The postcode entered by the user in the payment form

All of this information seems to be available in the Payment Intent object but none of the several Stripe guides specify how to capture them on the server. I want to avoid using webhooks because they seem like overkill for grabbing and persisting data that I'm already retrieving.

It also doesn't help that, thanks to how the Stripe documentation's AJAX/PHP solution is set up, trying to dump and die any variables on the server-side causes the entire client-side flow to break, stopping the payment form from rendering and blocking any debugging information. Essentially, this makes the entire implementation of Payment Intents API impossible to debug on the server.

Does anyone who's been here before know how I would go about capturing this information?

Relevant portion of JavaScript/AJAX:

const stripe = Stripe(<TEST_PUBLISHABLE_KEY>);
const fonts = [
    {
        cssSrc:
            "https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap",
    },
];
const appearance = {
    theme: "stripe",
    labels: "floating",
    variables: {
        colorText: "#2c2c2c",
        fontFamily: "Open Sans, Segoe UI, sans-serif",
        borderRadius: "4px",
    },
};

let elements;
initialize();
checkStatus();
document
    .querySelector("#payment-form")
    .addEventListener("submit", handleSubmit);

// Fetches a payment intent and captures the client secret

async function initialize() {
    const { clientSecret } = await fetch("/payment/stripe", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-CSRF-TOKEN": document.querySelector('input[name="_token"]').value,
        },
    }).then((r) => r.json());
    elements = stripe.elements({ fonts, appearance, clientSecret });
    const paymentElement = elements.create("payment");
    paymentElement.mount("#payment-element");
}

async function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    const { error } = await stripe.confirmPayment({
        elements,
        confirmParams: {
            // Make sure to change this to your payment completion page
            return_url: "http://localhost.rc/success"
        },
    });

    if (error.type === "card_error" || error.type === "validation_error") {
        showMessage(error.message);
    } else {
        showMessage("An unexpected error occured.");
    }
    setLoading(false);
}

// Fetches the payment intent status after payment submission
async function checkStatus() {
    const clientSecret = new URLSearchParams(window.location.search).get(
        "payment_intent_client_secret"
    );
    if (!clientSecret) {
        return;
    }
    const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
    switch (paymentIntent.status) {
        case "succeeded":
            showMessage("Payment succeeded!");
            break;
        case "processing":
            showMessage("Your payment is processing.");
            break;
        case "requires_payment_method":
            showMessage("Your payment was not successful, please try again.");
            break;
        default:
            showMessage("Something went wrong.");
            break;
    }
}

Routes file:

Route::post('/payment/stripe', [TransactionController::class, "stripe"]);

TransactionController:

public function stripe(Request $request) {
    
    Stripe\Stripe::setApiKey(env(<TEST_SECRET_KEY>)); 

    header('Content-Type: application/json');

    try {
       
        $paymentIntent = Stripe\PaymentIntent::create([
            'amount' => 2.99,
            'currency' => 'gbp',
            'automatic_payment_methods' => [
                'enabled' => true,
            ], 

        ]);

        $output = [
            'clientSecret' => $paymentIntent->client_secret,
        ];

        $this->storeStripe($paymentIntent, $output);

        echo json_encode($output);

    } catch (Stripe\Exception\CardException $e) {

    echo 'Error code is:' . $e->getError()->code;

    $paymentIntentId = $e->getError()->payment_intent->id;
    $paymentIntent = Stripe\PaymentIntent::retrieve($paymentIntentId);

    } catch (Exception $e) {

        http_response_code(500);
        echo json_encode(['error' => $e->getMessage()]);

    }
}

How can I capture the above information from the Payment Intent to store in my database?



Solution 1:[1]

At the recommendation of RyanM I opted instead for the webhook solution, which turned out to be easier than I expected using Spatie's Stripe Webhooks package (although it seems to be run by someone who cares more about closing issues than fixing potential bugs, so opting for Stripe Cashier instead would probably be both easier and a more pleasant developer experience).

Note that by default Stripe webhooks return an Event object that itself contains other objects relevant to the event, such as a PaymentIntent for payment_intent.succeeded, for example, and any associated Charge objects. Therefore it's necessary to drill down a little to get all of the information needed.

$paymentIntent = $this->webhookCall->payload["data"]["object"];
$paymentID = $this->webhookCall->payload["data"]["object"]["id"]; // Transaction ID
$charge = $this->webhookCall->payload["data"]["object"]["charges"]["data"][0];

$transaction = Transaction::where("gateway_payment_id", $paymentID)->first();

$transaction->payment_status = strtoupper($paymentIntent["status"]);      // Payment status
$transaction->payment_method = $charge["payment_method_details"]["type"]; // Payment method
$transaction->amount = ($paymentIntent["amount_received"]/100);           // Amount charged, in pounds
$transaction->currency = strtoupper($paymentIntent["currency"]);          // Currency charged in 
$transaction->postcode = $charge["billing_details"]["address"]["postal_code"] ?? "N/A";  // Postcode if entered by the user - otherwise default to N/A

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Hashim Aziz