Skip to content

WooCommerce Store API · public alpha

WooCommerce Store API payments without payment_data.

The Store API's checkout endpoint copies your JSON into $_POST and calls the gateway plugin's form handler. Keep the Store API for the cart and send the payment to Flint: a request body with a published schema, a dated API version, 3D Secure on your own page, and official PHP and Node SDKs.

3.79% + 35¢ on cards · no monthly fee · free sandbox · no credit card

The Store API checkout request

what your storefront sends
POST /wp-json/wc/store/v1/checkout
Cart-Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...

{
  "billing_address":  { "first_name": "Ada", ... },
  "shipping_address": { "first_name": "Ada", ... },
  "payment_method": "stripe",
  "payment_data": [
    { "key": "wc-stripe-confirmation-token",
      "value": "ctoken_1Example" },
    { "key": "wc_stripe_selected_upe_payment_type", "value": "card" }
  ]
}

payment_data is a list of string pairs. The keys are the form field names the installed Stripe gateway plugin reads from $_POST, and none of them are in the Store API schema.

flintpay/flint
use Flint\{Client, ClientOptions, RequestOptions};

$flint = new Client(new ClientOptions(
    baseUrl: 'https://api.withflintpay.com',
    apiKey: getenv('FLINT_API_KEY'),
));

// The browser posted back a single-use credential from
// Stripe Elements. Charge what the order still owes.
$result = $flint->orders->pay([
    'order_id' => $orderId,
    'body' => [
        'action' => 'pay',
        'payment_source' => ['token' => $paymentMethodId],
        'expected_outstanding_money' => [
            'amount' => 16632,
            'currency' => 'USD',
        ],
        'buyer_contact' => ['email' => $email],
    ],
], new RequestOptions(idempotencyKey: "pay-$cartKey-1"));

echo $result->payment_attempt->status; // succeeded

The same payment on Flint, from the PHP server you already run. Every field is in the published OpenAPI spec, and a field the endpoint does not accept is rejected by name.

quoted from source, checked 2026-09-20 · WooCommerce trunk and woocommerce-gateway-stripe develop

01payment_data

What WooCommerce does with payment_data.

POST /wp-json/wc/store/v1/checkout takes a payment_method and a payment_data list. Core handles them in three steps, quoted from WooCommerce trunk.

  1. The keys are sanitized, and every value becomes a string

    StoreApi/Utilities/CheckoutTrait.php

    sanitize_key lowercases each key and strips anything outside a to z, 0 to 9, underscore, and hyphen. wc_clean runs on each value. The result is a flat array of strings, the shape of a submitted HTML form.

    CheckoutTrait.php
    // StoreApi/Utilities/CheckoutTrait.php
    foreach ( $request['payment_data'] as $data ) {
        $payment_data[ sanitize_key( $data['key'] ) ]
            = wc_clean( $data['value'] );
    }
  2. The gateway is looked up in the classic registry

    StoreApi/Routes/V1/Checkout.php

    Any enabled WC_Payment_Gateway can be named as payment_method. Blocks integration governs which scripts a WordPress page loads, so it has no part in this check.

    Checkout.php
    // StoreApi/Routes/V1/Checkout.php
    $available_gateways =
        WC()->payment_gateways->get_available_payment_gateways();
    
    if ( ! isset( $available_gateways[ $request_payment_method ] ) ) {
        throw new RouteException(
            'woocommerce_rest_checkout_payment_method_disabled',
            /* ... */
            400
        );
    }
  3. The array is assigned to $_POST

    StoreApi/Legacy.php

    When no gateway handles the process-payment-with-context hook, WooCommerce assigns your payment data to the $_POST superglobal and calls the gateway's process_payment, the same method a WordPress checkout form submission reaches. The comment on the assignment is WooCommerce's own.

    Legacy.php
    // StoreApi/Legacy.php
    public function process_legacy_payment(
        PaymentContext $context,
        PaymentResult &$result
    ) {
        // ...
        // Add the payment data from the API to the POST global.
        $_POST = $context->payment_data;
    
        // Call the process payment method of the chosen gateway.
        $payment_method_object =
            $context->get_payment_method_instance();
    
        $payment_method_object->validate_fields();
    
        $gateway_result = $payment_method_object->process_payment(
            $context->order->get_id()
        );

Your storefront is sending a WordPress form submission as JSON. The fields it has to fill in are the $_POST keys that one gateway plugin reads, in the plugin version installed on that site.

On Flint

The request body is the contract.

Every request field is in a published OpenAPI spec, and the PHP and Node SDKs are generated from it.

POST /v1/orders/{order_id}/pay

A field the endpoint does not accept is rejected by name, so a misspelled key fails on your first sandbox request.

400 UNKNOWN_FIELD

Every write takes an idempotency key, so a retried request never charges the buyer twice.

Idempotency-Key

The balance the buyer approved is checked before the card is charged.

expected_outstanding_money

Browse the API reference →

02The keys

The payment_data keys change with the plugin.

WooCommerce's Checkout API docs still publish stripe_source as the Stripe example. The class that takes card payments in the current Stripe gateway reads a different set.

The documented Stripe example
// WooCommerce docs, Checkout API, the Stripe example
{ "key": "stripe_source",
  "value": "[a_stripe_payment_source]" },
{ "key": "paymentMethod", "value": "stripe" },
{ "key": "wc-stripe-new-payment-method", "value": true }

WC_Stripe_UPE_Payment_Gateway, which handles card payments today, never reads stripe_source.

What the current gateway reads
wc-stripe-confirmation-token
wc_payment_intent_id
wc_stripe_selected_upe_payment_type
wc-stripe-payment-token          // reusing a saved card
issavedtoken
save_payment_method

Read from the plugin's source. These are form field names, and no schema lists them.

The card key has been stripe_source, then wc-stripe-payment-method, and is now wc-stripe-confirmation-token. wc-stripe-is-deferred-intent was added along the way and no longer appears anywhere in the plugin. None of these changes carried a version or a deprecation window, because the keys are internal to the plugin.

The Store API docs say the same about every gateway: "We cannot comprehensively list all expected requests for all payment gateways." Their own checkout examples pay with cheque.

On Flint

Your integration stays on the API version it was built against.

Flint versions its API by date. A new version ships only when a release contains a breaking change, and Flint keeps serving the request and response shapes your code already parses. Both SDKs send the version their types were generated for on every request, so a plugin update on someone else's schedule cannot break your checkout.

You move forward when you choose to: one request, one webhook endpoint, or the whole account at a time, with 72 hours to undo it. API versions and upgrades →

API versions

Current version
2026-09-07
Also served
2026-02-01
Pin one request
Flint-Version: 2026-09-07
Pin a webhook endpoint
api_version
Version you were served
meta.api_version
Undo an upgrade
72 hours

033D Secure

3D Secure stays on your page.

A card needs authentication. On the Store API, the Stripe gateway answers with a URL on the WordPress site. On Flint, the pay call answers with a typed action that your checkout page runs.

Store API checkout response · 200 OK
HTTP/1.1 200 OK

{
  "order_id": 1042,
  "status": "pending",
  "payment_result": {
    "payment_status": "success",
    "payment_details": [
      { "key": "verification_endpoint",
        "value": "https://shop.example.com/?wc-ajax=wc_stripe_verify_intent&order=1042&nonce=8f2c1b9e4d&intent_id=pi_1Example" }
    ]
  }
}

payment_status is success while the order is still pending.

The buyer leaves your storefrontThe host in verification_endpoint is home_url(), the WordPress site, and the URL carries a WordPress nonce. Finishing authentication sends the buyer to the WordPress domain, through a wc-ajax endpoint, and back through redirect_to.

A storefront can skip the redirect by reading payment_intent_secret from the same response and calling Stripe.js itself. The order status change that the WordPress endpoint would have made then depends on the gateway's own Stripe webhook. An open issue on the gateway, #5691 from July 2026, reports payments that succeed at Stripe while the WooCommerce order stays pending and is then canceled.

woocommerce-gateway-stripe
// woocommerce-gateway-stripe, class-wc-stripe-blocks-support.php
$verification_endpoint = add_query_arg(
    [
        'order'       => $context->order->get_id(),
        'nonce'       => wp_create_nonce( 'wc_stripe_confirm_pi' ),
        'intent_id'   => $payment_details['payment_intent_id'],
        'redirect_to' => rawurlencode( $result->redirect_url ),
    ],
    home_url() . \WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
);

$payment_details['verification_endpoint'] = $verification_endpoint;
$result->set_payment_details( $payment_details );
$result->set_status( 'success' );

Hooked at priority 9999, after the legacy payment path has run.

On Flint

The pay call returns the action. Your page runs it.

Flint pay response · 200 OK
{
  "data": {
    "payment_attempt": {
      "order_payment_attempt_id": "opat_1kmn0aExample",
      "status": "requires_action",
      "is_resumable": true,
      "pending_actions": [
        {
          "action_type": "payment_authentication",
          "client_action": {
            "stripe": {
              "account_id": "acct_1kmn0aExample",
              "publishable_key": "pk_test_1kmn0aExample",
              "payment_intent": {
                "stripe_js_call": "handle_next_action",
                "client_secret": "pi_1kmn0aExample_secret_1kmn0aExample"
              }
            }
          }
        }
      ]
    }
  }
}

The attempt says what it is waiting for, names the Stripe.js call to run, and carries the client secret for that one step. No other host appears in it.

browser, then backend
// in the browser, on your checkout page
const action = attempt.pending_actions[0].client_action.stripe;
await stripe.handleNextAction({
  clientSecret: action.payment_intent.client_secret,
});

// then your backend resumes the same attempt. No new credential.
await flint.orders.pay({
  order_id: orderId,
  body: { action: "resume", order_payment_attempt_id: attemptId },
});
One attempt, start to finish
The whole challenge happens on shop.example.com. The order is paid once, by the attempt that started it.

04The result

The pay response says what happened and what to do next.

The Store API returns a payment_status and a list of string pairs whose meaning belongs to the gateway plugin. Flint returns the order and the payment attempt: its status, whether it can be resumed, and why a card was declined.

Store API payment_result

payment_status
success, pending, failure, or error
payment_details
key and value strings, set by the plugin
During 3D Secure
success, with the order pending

From StoreApi/Schemas/V1/CheckoutSchema.php. The schema types every payment detail as a string.

last_payment_error.code · a closed set of 18

  • card_declined
  • insufficient_funds
  • expired_card
  • incorrect_cvc
  • authentication_required
  • processing_error
  • payment_blocked
  • payment_method_declined
  • payment_method_unavailable
  • payment_method_temporarily_unavailable
  • payment_not_completed
  • payment_action_expired
  • bank_account_closed
  • bank_account_not_found
  • bank_debit_not_authorized
  • bank_account_restricted
  • bank_debit_limit_exceeded
  • payment_failed

Flint maps processor-specific decline reasons into this set, so your checkout branches on a code and writes its own buyer-facing message. Declines and payment attempts →

Flint pay response · 200 OK
{
  "data": {
    "order": {
      "order_id": "ord_1kmn0aExample",
      "status": "closed",
      "payment_status": "paid",
      "settlement_amounts": {
        "paid_money": { "amount": 16632, "currency": "USD" },
        "outstanding_money": { "amount": 0, "currency": "USD" }
      }
    },
    "payment_attempt": {
      "order_payment_attempt_id": "opat_1kmn0aExample",
      "status": "succeeded",
      "is_resumable": false
    }
  }
}

The order and the attempt come back together, so the same response that confirms the charge also shows the balance at zero.

Every outcome has a field

The card was charged and the order is paid.

payment_attempt.status "succeeded"

The issuer declined. The reason is in the same response, and the order stays open for another card.

payment_intents[].last_payment_error.code

The issuer wants 3D Secure. The attempt carries the action for your page to run.

pending_actions[].action_type

The connection dropped mid-payment. Resume the attempt and Flint reconciles from its own record, so the buyer is never charged twice.

status "requires_retry" · is_resumable

The cart changed after the buyer saw the total. Flint refuses before charging.

409 ORDER_CHANGED_REFRESH_REQUIRED

05The paid order

One webhook puts the paid order into WooCommerce admin.

Your team keeps fulfilling from the admin they know. When the order is paid, Flint sends order.paid to your backend, and your handler creates the WooCommerce order through its REST API, already marked paid.

POST /webhooks/flint
{
  "webhook_event_id": "whev_1kmn0aExample",
  "event_type": "order.paid",
  "payload_version": 1,
  "mode": "live",
  "merchant_id": "mer_1kmn0aExample",
  "created_at": "2026-09-19T14:00:00Z",
  "data": {
    "order_id": "ord_1kmn0aExample",
    "status": "closed",
    "payment_status": "paid",
    "total_money": { "amount": 16632, "currency": "USD" },
    "paid_money": { "amount": 16632, "currency": "USD" },
    "outstanding_money": { "amount": 0, "currency": "USD" }
  }
}

Signed, and both SDKs verify the signature against the raw request body.

Into WooCommerce
POST /wp-json/wc/v3/orders
Authorization: Basic <consumer key : consumer secret>

{
  "payment_method": "flint",
  "payment_method_title": "Card via Flint",
  "set_paid": true,
  "transaction_id": "ord_1kmn0aExample",
  "billing":  { "first_name": "Ada", "email": "ada@example.com" },
  "line_items": [
    { "product_id": 482, "quantity": 1 },
    { "product_id": 517, "quantity": 2 }
  ]
}

The Flint order id goes in transaction_id, so support gets from a WooCommerce order to the payment that settled it in one step.

On Flint

The paid signal arrives, and the write-back runs once.

A failed delivery is retried on a fixed backoff, up to 9 attempts over about three days.

order.paid

Every retry and resend carries the same event id. Store it, and the WooCommerce order is created once.

webhook_event_id

An event that runs out of retries stays queryable, and you resend it after fixing your handler.

POST /v1/webhook-deliveries/{webhook_delivery_id}/resend

Your WooCommerce cart key travels on the Flint order, so either side finds the other.

GET /v1/orders?external_reference_id={id}

Receive events on localhost

flint listen --forward-to http://localhost:8080/webhooks/flint

No tunnel and no public URL while you build. The CLI mints a signing secret for the session, so you test the same signature check you will run in production. Webhooks guide →

06The cart

Your Store API cart becomes the order.

The cart half of the Store API stays where it is. Your storefront keeps adding items, applying coupons, and selecting shipping rates over JSON, and at checkout your backend sends the cart's lines to Flint.

  • /cart/add-item
  • /cart/apply-coupon
  • /cart/select-shipping-rate
  • /cart/update-customer
From the Store API
GET /wp-json/wc/store/v1/cart
Cart-Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...

{
  "items": [
    {
      "key": "a5771bce93e200c36f7cd9dfd0e5deaa",
      "id": 482,
      "quantity": 1,
      "name": "Merino crew",
      "prices": {
        "price": "11800",
        "currency_code": "USD",
        "currency_minor_unit": 2
      }
    },
    {
      "key": "b6d767d2f8ed5d21a44b0e5886680cb9",
      "id": 517,
      "quantity": 2,
      "name": "Wool socks",
      "prices": {
        "price": "1800",
        "currency_code": "USD",
        "currency_minor_unit": 2
      }
    }
  ]
}

Store API prices are strings of integer minor units, so 11800 becomes a Flint amount with a cast and no arithmetic.

To Flint
curl -X POST https://api.withflintpay.com/v1/orders \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: order-cart-a1b2c3" \
  -d '{
    "external_reference_id": "wc-cart-a1b2c3",
    "line_items": [
      {
        "name": "Merino crew",
        "quantity": 1,
        "unit_price_money": { "amount": 11800, "currency": "USD" },
        "tax": { "taxable": true }
      },
      {
        "name": "Wool socks",
        "quantity": 2,
        "unit_price_money": { "amount": 1800, "currency": "USD" },
        "tax": { "taxable": true }
      }
    ],
    "delivery_destination": {
      "address": {
        "line1": "540 Broad Street",
        "city": "Augusta",
        "state": "GA",
        "postal_code": "30901",
        "country": "US"
      }
    },
    "tax": {
      "enabled": true,
      "calculation": { "mode": "automatic", "price_mode": "additive" }
    }
  }'
what Flint computed
Merino crew, quantity 1line_items[0]$118.00
Wool socks, quantity 2line_items[1]$36.00
Subtotalpricing_amounts.subtotal_money$154.00
Tax, Augusta GApricing_amounts.tax_money$12.32
Totalpricing_amounts.total_money$166.32
Still owedsettlement_amounts.outstanding_money$166.32
You send names, quantities, prices, and the shipping address. Flint computes the tax and the total, and holds the balance every payment settles against.

The Cart-Token your backend reads with

Your backend reads the cart with the same Cart-Token the browser holds, so the prices on the Flint order come from WooCommerce and never from the browser.

Cart-Token, decoded
Cart-Token: <HS256 JWT, signed with '@' . wp_salt()>

{
  "user_id": "t_5f2a91c4e8",
  "exp": 1753488000,        // issued + DAY_IN_SECONDS * 2, so 48h
  "iss": "store-api"
}

From CartTokenUtils.php and JsonWebToken.php.

The session your storefront holds

Token lifetime
48h, filtered by wc_session_expiration
Signature
HS256 over '@' . wp_salt()
Nonce alternative
server-side wp_create_nonce only
Checkout rate limit
3 requests / 60s when enabled
Guest to logged-in
cart merge, issue #55653, open
Headless WooCommerce checkout, end to end: tax, shipping, refunds, and saved cards

reviewed 2026-09-20 against the published Flint API and WooCommerce trunk and woocommerce-gateway-stripe develop · corrections: Flint Help

What comes with the keys

Stripe still processes every card.

Card data goes straight from your checkout page to Stripe, with the same PCI scope, fraud detection, and dispute handling as a direct integration. Flint provisions and runs the processing account, so you sign up once, hold one set of keys, and pay one processing fee per payment.

574
API operations on one set of keys
92
resource families, orders to payouts
211
webhook event types to subscribe to
20
dashboard sections before you write a line

API counts come from the published OpenAPI spec · every Flint code sample is checked against it at build time

Public alpha

Alpha accounts get new features first.

Merchants take real payments on Flint today, and new features reach alpha accounts before general rollout. The integration you write this week stays on the API version it was written for.

shipped 2026-09-29 · Codes for saved details default to the auto channel

What alpha accounts get →

Building now?

Create an account from your terminal with flint signup. The sandbox is free, with no credit card.

07Start

From a sandbox key to a paid test order in ten minutes.

PHP

composer require flintpay/flint:0.3.0-beta.1

Node

npm install @flintpay/node

CLI

npm install -g @flintpay/cli

Test keys are prefixed flint_test_ and bound to a sandbox with its own data, so nothing you do there touches live money. Live keys are prefixed flint_live_, on the same host and the same paths. Pay with 4242 4242 4242 4242 first, then with 4000 0025 0000 3155 to run a 3D Secure challenge on your own page.

FAQ

Before you sign up.

What is payment_data in the WooCommerce Store API checkout endpoint?

A list of key and value string pairs sent to POST /wp-json/wc/store/v1/checkout. WooCommerce sanitizes each key with sanitize_key, cleans each value with wc_clean, and in the legacy payment path assigns the result to the $_POST superglobal before calling the chosen gateway's process_payment method. Which keys a gateway expects is defined by that plugin's code, so the payload differs per gateway and per plugin release. On Flint, the pay request body is defined in a published OpenAPI spec, and a field the endpoint does not accept returns 400 UNKNOWN_FIELD.

Why does the stripe_source example in the WooCommerce docs not work?

WC_Stripe_UPE_Payment_Gateway, the class that takes card payments in the current Stripe gateway plugin, does not read stripe_source. It reads wc-stripe-confirmation-token. The Checkout API documentation still shows stripe_source, paymentMethod, and wc-stripe-new-payment-method as the Stripe example. The card key has been stripe_source, then wc-stripe-payment-method, and is now wc-stripe-confirmation-token, and none of those changes came with a version, because the keys are form field names inside a plugin. Flint versions its API by date and keeps serving the version your integration was built against.

Does a payment gateway need Blocks support to work with the Store API?

No. The eligibility check in the checkout route is WC()->payment_gateways->get_available_payment_gateways(), the classic registry, so any enabled WC_Payment_Gateway can be named as payment_method. If no gateway handles the process-payment-with-context hook, WooCommerce falls back to the legacy path and calls process_payment directly. The Blocks IntegrationInterface governs client-side asset registration, which is why a gateway can be reachable over the API while its wallet buttons and its 3D Secure handling stay out of reach for a frontend that is not WordPress. On Flint, the card fields, Apple Pay, Google Pay, and 3D Secure all run in Stripe Elements on your own page.

What happens with 3D Secure in a headless WooCommerce checkout?

The Stripe gateway returns HTTP 200 with payment_status success and puts a verification_endpoint into payment_details. That URL is built from home_url() plus a WooCommerce AJAX endpoint and carries a WordPress nonce, so finishing authentication means sending the buyer to the WordPress domain and back. On Flint, the pay call returns a typed pending action with a client secret scoped to that one step. Your page passes it to the Stripe.js handleNextAction call, the buyer authenticates there, and your backend resumes the same payment attempt.

How long does a Store API Cart-Token last?

48 hours by default. It is an HS256 JSON Web Token signed with an at sign prepended to wp_salt(), carrying user_id, exp, and an iss of store-api, with the expiry set to the current time plus DAY_IN_SECONDS times two and filterable through wc_session_expiration. Your storefront keeps using it with Flint: the cart stays on the Store API, and your backend reads it with the same token to create the Flint order.

Can I keep WooCommerce and take payments through Flint?

Yes. WooCommerce stays the catalog, the cart, and the admin. Your backend creates a Flint order from the Store API cart, the buyer pays on your domain, and your order.paid webhook handler writes the paid order into WooCommerce through its REST API with set_paid true. There is no plugin to install or keep updated.

Is there a PHP SDK?

Yes. flintpay/flint is the official PHP SDK, for PHP 8.2 and later with the cURL and JSON extensions and no framework dependency. It is generated from the same API contract as the Node SDK, @flintpay/node, so every public operation is a method in both, and both verify webhook signatures for you. The API is plain HTTP and JSON, so neither is required.

Who processes the cards?

Stripe processes every card. Card data goes directly from your checkout page to Stripe, with the same PCI scope, fraud detection, and dispute handling as a direct Stripe integration. Flint provisions and operates the processing account, so there is one signup, one set of keys, and one fee per payment.

What does it cost?

Cards cost 3.79% + 35¢, with no monthly fee. The sandbox is free and needs no credit card. One processing fee per payment covers Flint and the card processing underneath, so there is no separate processor bill.

How do I get started?

Request access and we email you a link to create your account, or create one now from your terminal with flint signup. The sandbox is free, with no credit card and no sales call. Flint is in public alpha and onboards US businesses today; buyers can pay from anywhere. Install the PHP or Node SDK, create an order from a test cart, and pay it with the 4242 test card in about ten minutes. Then pay with 4000 0025 0000 3155 to run a 3D Secure challenge on your own page. Quickstart

Take your next test payment without payment_data.

Free sandbox, no credit card, no sales call. WooCommerce keeps the cart and the admin, and your first paid test order takes about ten minutes.

Skip the wait: npm install -g @flintpay/cli && flint signup creates your account and a sandbox key from the terminal. CLI guide

POST /v1/orders/{order_id}/pay · pay your first order