Articles on: E-commerce

Integrating Core payments into WooCommerce: developer guide

Integrating Core payments into WooCommerce: developer guide


This guide is for the developer setting up Core by Carlo on a WooCommerce store. It assumes you are comfortable writing a WordPress plugin. Core is integrated as a WooCommerce payment gateway using the hosted payment page flow. The customer is redirected to a Core-hosted page, pays there, and returns to the store. Card details never touch the merchant's server, so the PCI scope stays minimal.


Before you start:



1. The flow


  1. Customer places the order. WooCommerce calls your gateway's process_payment().
  2. Your plugin calls POST /transactions on the Core API and receives a paymentPageUrl.
  3. WooCommerce redirects the customer to that URL.
  4. The customer pays. Core redirects them to your success or failure URL.
  5. Core sends the final status to your callback URL.
  6. Your plugin verifies that status against the Core API, then marks the order as paid.

Step 6 is not optional. See section 6.


2. Prerequisites


  • Sandbox access is created by Core on request, not by self-registration.
  • Once your environment is set up, you will find your API key in the Partner Dashboard under Developer → Configuration, along with the three URLs to configure: success, failure, and callback.

Base URLs:


Sandbox

Production

Auth

https://sandbox-api.corebycarlo.com/api/v1/auth/partner

https://api.corebycarlo.com/api/v1/auth/partner

API

https://sandbox-api.corebycarlo.com/api/v1/partner

https://api.corebycarlo.com/api/v1/partner

All requests are JSON. All endpoints except login require a Bearer token.


3. Plugin structure


Register the gateway class after plugins load, and declare it to WooCommerce:

add_action( 'plugins_loaded', 'core_init_gateway' );

function core_init_gateway() {
class WC_Gateway_Core extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'core_by_carlo';
$this->method_title = 'Core by Carlo';
$this->method_description = 'Accept card payments with Core by Carlo.';
$this->has_fields = false;
$this->supports = array( 'products', 'refunds' );

$this->init_form_fields();
$this->init_settings();

$this->title = $this->get_option( 'title' );

add_action(
'woocommerce_update_options_payment_gateways_' . $this->id,
array( $this, 'process_admin_options' )
);
}
}
}

add_filter( 'woocommerce_payment_gateways', function ( $methods ) {
$methods[] = 'WC_Gateway_Core';
return $methods;
} );

has_fields stays false: there is no card form on your site.
Your settings screen should expose the merchant's credentials, the API key, and a sandbox toggle. Store credentials with the WooCommerce Settings API and never expose them client-side.
Currency guard. Core settles in euros and the transaction request has no currency field. Add an availability check so the method disappears on a non-euro store rather than charging the wrong amount:

public function is_available() {
return parent::is_available() && get_woocommerce_currency() === 'EUR';
}


4. Authentication


Call the login endpoint with the merchant credentials and API key to obtain a Bearer token. Check the Login page of the API reference for the exact request body.
The token is valid for 90 days. Do not request one per transaction. Cache it in a transient and refresh it when a call comes back 401:

$token = get_transient( 'core_access_token' );
if ( ! $token ) {
$token = $this->login();
set_transient( 'core_access_token', $token, 80 * DAY_IN_SECONDS );
}

Refreshing on failure is more reliable than trusting an expiry calculation.


5. Creating the payment


public function process_payment( $order_id ) {
$order = wc_get_order( $order_id );

$response = wp_remote_post( $this->api_url . '/transactions', array(
'headers' => array(
'Authorization' => 'Bearer ' . $this->get_token(),
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( array(
'amount' => (float) $order->get_total(),
'successUrl' => $this->get_return_url( $order ),
'failedUrl' => $order->get_checkout_payment_url(),
'orderReference' => (string) $order->get_id(),
'description' => 'Order #' . $order->get_order_number(),
'metadata' => array(
'customerEmail' => $order->get_billing_email(),
),
'paymentMethod' => 'CREDIT_CARD',
) ),
'timeout' => 30,
) );

if ( is_wp_error( $response ) ) {
wc_add_notice( 'Payment error: unable to reach the payment provider.', 'error' );
return array( 'result' => 'failure' );
}

$body = json_decode( wp_remote_retrieve_body( $response ), true );

if ( empty( $body['paymentPageUrl'] ) ) {
wc_add_notice( 'Payment error: could not start the payment.', 'error' );
return array( 'result' => 'failure' );
}

$order->update_meta_data( '_core_transaction_id', $body['id'] );
$order->save();

return array(
'result' => 'success',
'redirect' => $body['paymentPageUrl'],
);
}

Three things to note.

  • Store the transaction id on the order. You need it to verify the callback and to refund later.
  • Do not call payment_complete() here. The customer has not paid yet. Leave the order pending.
  • Payment sessions expire. The response carries an expiresAt. If the customer abandons and comes back, create a new session. Never reuse a stored paymentPageUrl.
  • Redirect, not iframe. 3D Secure requires a full-page redirect. Embedding the payment page in an iframe will break authentication.


6. Handling the callback, and why you must verify it


Core sends a POST to your callback URL when a transaction reaches a final state. Register a handler using the WooCommerce API endpoint:

add_action( 'woocommerce_api_wc_gateway_core', 'core_handle_callback' );

Your callback URL is then https://yourstore.com/?wc-api=wc_gateway_core.
This URL is public and guessable. Treat the payload as untrusted input.
The callback body is not signed. Anyone could send you a forged COMPLETED status and receive goods without paying. So never mark an order paid from the payload. Read only the transaction id from it, then ask the Core API what actually happened:

function core_handle_callback() {
$payload = json_decode( file_get_contents( 'php://input' ), true );

$transaction_id = $payload['transaction']['id'] ?? null;
if ( ! $transaction_id ) {
status_header( 400 );
exit;
}

// Never trust the payload status. Ask the API.
$transaction = core_fetch_transaction( $transaction_id );

$order_id = $transaction['metadata']['orderReference'] ?? null;
$order = $order_id ? wc_get_order( $order_id ) : null;

if ( ! $order ) {
status_header( 404 );
exit;
}

// Idempotency: the same callback can arrive twice.
if ( $order->is_paid() ) {
status_header( 200 );
exit;
}

// Amount check: the order total must match what was actually paid.
if ( (float) $transaction['amount'] !== (float) $order->get_total() ) {
$order->update_status( 'on-hold', 'Core: amount mismatch, manual review required.' );
status_header( 200 );
exit;
}

if ( 'COMPLETED' === $transaction['status'] ) {
$order->payment_complete( $transaction_id );
} elseif ( in_array( $transaction['status'], array( 'FAILED', 'CANCELLED' ), true ) ) {
$order->update_status( 'failed', 'Core: payment ' . strtolower( $transaction['status'] ) );
}

status_header( 200 );
exit;
}

Return 200 quickly. If you have slow work to do, queue it.
Do not confirm orders from the success URL. A customer can close the browser before being redirected, and the redirect can be reached without a payment having succeeded. The verified callback is the only source of truth.

7. Refunds


Core cancels a transaction in full. There is no amount parameter, and the window is 3 days from the payment date.
WooCommerce, on the other hand, lets a merchant refund a single line item. You must reject that case explicitly, otherwise a merchant who tries to refund 30 EUR of a 90 EUR order will have the whole order cancelled without realising it:

public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );

if ( (float) $amount !== (float) $order->get_total() ) {
return new WP_Error(
'core_partial_refund',
'Core supports full refunds only. Refund the full order amount, or contact Core support for a partial refund.'
);
}

// PUT /transactions/{id}/cancel with an optional comment
// ...
}

Also handle the 30-day limit: past that window the API will refuse, and the merchant deserves a clear message rather than a silent failure.

8. Checkout Blocks compatibility


WooCommerce's default checkout is block-based. A gateway registered only through the PHP class will not appear there.
You need two extra pieces:

  • A server-side class extending Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType, registered on the woocommerce_blocks_payment_method_type_registration hook.
  • A client-side script calling registerPaymentMethod from the blocks registry, with a build step that externalises the WooCommerce packages.

The name on the JS side and the $name on the PHP side must match your gateway id.
Payment processing itself stays where it is. The Checkout block converts the submitted payment data and calls your existing process_payment(), so you do not duplicate any logic.
You should also declare the plugin compatible with WooCommerce's High-Performance Order Storage, otherwise merchants will see an incompatibility warning in their admin.

9. Testing


If you have not requested your sandbox access yet, do it now: support@corebycarlo.com. You cannot test without it.
Sandbox transactions do not move real money. Test cards are listed in the Test Cards page of the API reference.


Test at minimum:

  • A successful payment, end to end
  • A failed payment, and that the order goes to failed rather than staying pending forever
  • A customer who closes the browser after paying but before the redirect. The order must still be marked paid by the callback.
  • A duplicated callback. The order must not be processed twice.
  • A forged callback: send yourself a COMPLETED payload for an unpaid order. The order must not be marked as paid. If it is, your verification step is not working.
  • A full refund
  • An attempted partial refund, which must fail with a readable message

One caveat on the sandbox: it simulates the acquirer, so failure scenarios are not always reproducible. Mock your own client where you need to test failure handling.


10. Go-live checklist


  • ✅ Production credentials and API key configured
  • ✅ Success, failure and callback URLs set in the production dashboard
  • ✅ Callback URL is HTTPS
  • ✅ Callback verification against the API confirmed working
  • ✅ Idempotency check in place
  • ✅ Amount comparison in place
  • ✅ Store currency is EUR, and the currency guard is active
  • ✅ Partial refunds rejected with a clear message
  • ✅ Gateway visible and working on the block checkout
  • ✅ HPOS compatibility declared
  • ✅ Credentials stored server-side only, never logged
  • ✅ One real low-value transaction tested in production, then refunded


Questions during integration: support@corebycarlo.com

Updated on: 05/10/2026