---
title: "woocommerce_checkout_order_created Hook: Docs & Timing"
description: "woocommerce_checkout_order_created documentation: the signature, where it sits in the checkout hook sequence, and why it never fires on the blocks checkout."
url: "https://wpwebhooks.org/blog/woocommerce-checkout-order-created/"
date: "2026-07-31"
---

# woocommerce_checkout_order_created Hook: Docs & Timing

**TL;DR:** `woocommerce_checkout_order_created` fires once the order row exists — and only on the shortcode checkout.

-   Signature: `do_action( 'woocommerce_checkout_order_created', $order )` — one parameter, a `WC_Order`. Added in WooCommerce 4.3.0.
-   It runs inside `WC_Checkout::create_order()`, immediately after `$order->save()` and after the order meta hook.
-   The blocks checkout never reaches that method, so it fires `woocommerce_store_api_checkout_order_created` instead.
-   It also misses admin-created, API-created and subscription renewal orders.
-   If you need _every_ order, bind to a status transition, not to a checkout hook.

/ Overview

## What is **woocommerce\_checkout\_order\_created**?

It is the action WooCommerce fires the moment a checkout order has been written to the database. It lives at the end of `WC_Checkout::create_order()` in [class-wc-checkout.php](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/class-wc-checkout.php), carries an `@since 4.3.0` tag, and is the earliest point in the checkout at which you are holding a saved order with a real ID.

That last part is what makes it useful. Its neighbour `woocommerce_checkout_create_order` runs a few lines earlier and gives you the order before it is saved, which is the right place to change it and the wrong place to tell anything about it — the order has no ID yet, and an exception thrown later can still discard it entirely.

/ Signature

## What is the exact **signature**?

One parameter. That is the whole surface:

PHP — class-wc-checkout.php

```
/**
 * Action hook fired after an order is created.
 *
 * @since 4.3.0
 */
do_action( 'woocommerce_checkout_order_created', $order );
```

| Parameter | Type | What it holds |
| --- | --- | --- |
| `$order` | `WC_Order` | The saved order object, with a real ID, line items, totals and addresses already persisted. |

Passing the object rather than an ID is a small convenience with a real consequence: you can read totals and items without a `wc_get_order()` round trip, and you are guaranteed to be looking at the same instance the checkout just saved rather than a re-read that might miss something still in memory.

/ Sequence

## Where does it sit in the **checkout hook sequence**?

Fourth, in a run of hooks that all fire within a few lines of each other. The order is fixed and worth knowing, because picking the wrong neighbour is the most common reason a callback misbehaves:

| # | Hook | Parameters | Order saved? |
| --- | --- | --- | --- |
| 1 | `woocommerce_checkout_create_order` | `$order, $data` | No — modify it here |
| 2 | _$order->save()_ | — | The write itself |
| 3 | `woocommerce_checkout_update_order_meta` | `$order_id, $data` | Yes |
| 4 | `woocommerce_checkout_order_created` | `$order` | Yes |
| 5 | `woocommerce_checkout_order_processed` | `$order_id, $posted_data, $order` | Yes — later in the request |

FIG 01 — Where the hook sits, and the path that skips it

Between 2 and 4 there is a guard worth knowing about. Recent WooCommerce re-reads the persisted order and throws if the cart had items but the saved order has none, so a save that silently dropped every line item aborts the checkout rather than completing a paid-but-empty order. By the time hook 4 runs, that check has already passed.

/ Blocks

## Why does it never fire on the **blocks checkout**?

Because the blocks checkout does not call `WC_Checkout::create_order()`. It posts to the Store API, which builds the order through its own route and fires its own parallel actions — `woocommerce_store_api_checkout_order_created` and `woocommerce_store_api_checkout_order_processed`. WooCommerce states the rule plainly in its [hook alternatives documentation](https://developer.woocommerce.com/docs/block-development/reference/hooks/hook-alternatives/): hooks that fire in the shortcode process do not fire on Store API requests from the blocks.

This is the single biggest source of "my order hook stopped working" reports, and it usually arrives as a site change rather than a code change — someone swaps the shortcode checkout page for the Checkout block, and an integration that worked for two years goes quiet without a single error. Nothing throws. The callback is simply never called.

PHP — cover both checkout paths

```
$notify = function( $order ) {
    // One callback, both routes.
    my_enqueue_order_notification( $order->get_id() );
};

add_action( 'woocommerce_checkout_order_created', $notify );
add_action( 'woocommerce_store_api_checkout_order_created', $notify );
```

> Any other WC hooks that fire in the Shortcode process will not fire on Store API requests from the blocks. — WooCommerce block development documentation

/ Duplicates

## Can the same order fire this hook **twice**?

Yes, and this one catches almost everybody. `create_order()` does not always create an order. It first looks for an order the session is already awaiting payment on, and if that order still matches the cart — same items, same total, verified through a cart hash — and is still `pending` or `failed`, it resumes that order instead of making a new one.

PHP — the resume branch in create\_order()

```
$order_id  = absint( WC()->session->get( 'order_awaiting_payment' ) );
$cart_hash = WC()->cart->get_cart_hash();
$order     = $order_id ? wc_get_order( $order_id ) : null;

if ( $order && $order->has_cart_hash( $cart_hash )
     && $order->has_status( [ 'pending', 'failed' ] ) ) {

    do_action( 'woocommerce_resume_order', $order_id );
    $order->remove_order_items();  // re-added below

} else {
    $order = new WC_Order();
}
```

The hook at the end of the method does not know or care which branch ran. So a customer whose card is declined, who then presses pay again, produces two `woocommerce_checkout_order_created` calls carrying **the same order ID** — three attempts, three calls. If your callback posts to a CRM, that is three records for one order — and if it sends an [order SMS](https://wpwebhooks.org/blog/woocommerce-sms-order-notification-twilio/), three billed messages.

The defence is a marker on the order rather than a check in your own code path, because the second call happens in a completely separate request:

PHP — fire once per order, not once per attempt

```
add_action( 'woocommerce_checkout_order_created', function( $order ) {

    if ( $order->get_meta( '_my_notified' ) ) {
        return;  // resumed order, already sent
    }

    $order->update_meta_data( '_my_notified', time() );
    $order->save();

    my_enqueue_order_notification( $order->get_id() );
} );
```

Listening for `woocommerce_resume_order` is the other half of the picture if you want to distinguish a retry from a first attempt rather than just suppress it — it fires only on the resume branch, and only ever with the order ID.

/ Comparison

## How does it differ from **order\_processed** and **new\_order**?

By timing, payload, and how much of the world they cover. All three describe "an order happened", and they disagree about when that is:

| Hook | Fires when | Covers blocks? | Covers admin orders? |
| --- | --- | --- | --- |
| `woocommerce_checkout_order_created` | Order row written, inside create\_order() | No | No |
| `woocommerce_checkout_order_processed` | Later in the checkout request, with posted data | No | No |
| `woocommerce_new_order` | Any order is first persisted, from any source | Yes | Yes |
| `woocommerce_order_status_processing` | Order reaches the processing status | Yes | Yes |

So the choice follows from the question you are answering. Enriching a checkout order with metadata is a job for `order_created`, because you want the earliest saved state and you only care about checkout. Reacting to a paid order is a job for a status transition, because payment is what you actually care about and it happens on every path — including [gateway callbacks that arrive minutes later](https://wpwebhooks.org/blog/woocommerce-webhooks-action-scheduler/), long after the checkout request has ended.

/ Delivery

## What breaks if you send the request **inline**?

Checkout gets slower and less reliable, in that order. A blocking HTTP call in this hook runs inside the customer's checkout request, between the order being saved and the payment gateway being handed control. Every millisecond the endpoint takes is a millisecond the buyer spends on a spinner at the most abandonment-sensitive moment of the entire session.

Then there is what an exception does here. The hook fires inside a `try` block whose `catch` releases the order's coupons and discards the order. A callback that throws — a JSON encoding failure, a fatal in a third-party client library — does not just fail to notify: it can take down a checkout that had already succeeded. Wrapping your own callback in a `try/catch` is not defensive programming here, it is required.

The sound version is to record the intent and return. Write the order ID somewhere durable, let a background worker do the HTTP with retries and a log, and keep the checkout request doing nothing but checkout. That is the same conclusion the [built-in WooCommerce webhooks](https://woocommerce.com/document/webhooks/) reach — they queue deliveries rather than sending them in the request that created the order.

/Footnotes

¹ Hook position, `@since` tag and parameters verified in [class-wc-checkout.php](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/class-wc-checkout.php) against WooCommerce 10.8.1.

² Store API equivalents and the shortcode-only rule from the WooCommerce [hook alternatives](https://developer.woocommerce.com/docs/block-development/reference/hooks/hook-alternatives/) reference.

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"woocommerce_checkout_order_created Hook: Docs & Timing","description":"woocommerce_checkout_order_created documentation: the signature, where it sits in the checkout hook sequence, and why it never fires on the blocks checkout.","datePublished":"2026-07-31","dateModified":"2026-07-31","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/woocommerce-checkout-order-created/","image":{"@type":"ImageObject","url":"https://wpwebhooks.org/og_image.jpg","width":1200,"height":630,"caption":"woocommerce_checkout_order_created Hook: Docs & Timing"},"keywords":["woocommerce checkout order created","woocommerce order created hook","woocommerce checkout hooks","woocommerce store api checkout order created","woocommerce new order hook","wc checkout create order","woocommerce order hook sequence"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"woocommerce_checkout_order_created Hook: Docs & Timing","item":"https://wpwebhooks.org/blog/woocommerce-checkout-order-created/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What is woocommerce_checkout_order_created?","acceptedAnswer":{"@type":"Answer","text":"It is a WooCommerce action that fires inside WC_Checkout::create_order() immediately after the order has been saved to the database. It was introduced in WooCommerce 4.3.0 and receives a single parameter, the WC_Order object, which already has a real order ID at that point."}},{"@type":"Question","name":"What is the signature of woocommerce_checkout_order_created?","acceptedAnswer":{"@type":"Answer","text":"do_action('woocommerce_checkout_order_created', $order) — one parameter, a WC_Order instance. Because it passes the object rather than an ID, a callback can read totals, items and addresses without calling wc_get_order() first."}},{"@type":"Question","name":"Why does woocommerce_checkout_order_created not fire on the blocks checkout?","acceptedAnswer":{"@type":"Answer","text":"Because the blocks checkout does not go through WC_Checkout::create_order() at all. It posts to the Store API, which fires its own parallel actions, woocommerce_store_api_checkout_order_created and woocommerce_store_api_checkout_order_processed. WooCommerce documents that shortcode checkout hooks do not run on Store API requests."}},{"@type":"Question","name":"What is the difference between woocommerce_checkout_order_created and woocommerce_checkout_order_processed?","acceptedAnswer":{"@type":"Answer","text":"Timing and payload. order_created fires inside create_order() the moment the order row exists and passes only $order. order_processed fires later in the same request, after payment has been set up, and passes $order_id, $posted_data and $order. Use created for enrichment, processed when you need the posted checkout data."}},{"@type":"Question","name":"Which hook should trigger an order webhook?","acceptedAnswer":{"@type":"Answer","text":"For a notification that an order exists, woocommerce_checkout_order_created is the earliest safe point, but it misses blocks and admin-created orders. If you need every order regardless of origin, bind to a status transition such as woocommerce_order_status_processing instead, and mirror the Store API hook when you specifically want checkout-time behaviour."}}]}
```
