WooCommerce order status SMS: a paste-ready PHP snippet
Checked against the docs:
In WooCommerce you hook a function to woocommerce_order_status_processing or woocommerce_order_status_completed and call wp_remote_post on https://smsportal.app/api/v1/gateway/send-sms with an x-api-key header. The snippet takes the billing phone, adds your country code, writes the result to the order notes and never sends the same text twice.
What you need
- WooCommerce on WordPress with access to
wp-config.php(SFTP or hosting file manager). - Somewhere to put code: the Code Snippets plugin or a child theme
functions.php. - A smsportal account with an Android phone and SIM card connected.
- An API key from the smsportal dashboard (API Keys → Generate API Key).
How to send order SMS from WooCommerce
Put the key in wp-config.php
Above the line
/* That's all, stop editing! */adddefine( 'SMSPORTAL_API_KEY', 'your_api_key' );. The key stays out of the database, snippet exports and your theme repository.Paste the snippet
In Code Snippets add a new PHP snippet and let it run everywhere. Without the plugin, paste it at the end of your child theme
functions.php(the file already starts with<?php). Set your country code in thesmsportal_normalize_phonecall:1for US/Canada,44for the UK.PHP snippet: SMS on processing and completed// Order status SMS through smsportal. // Keep the API key in wp-config.php, not here: // define( 'SMSPORTAL_API_KEY', 'your_api_key' ); add_action( 'woocommerce_order_status_processing', function ( $order_id, $order ) { smsportal_order_sms( $order, 'processing', 'Order #{number} received. Thank you, {name}! Example Store' ); }, 10, 2 ); add_action( 'woocommerce_order_status_completed', function ( $order_id, $order ) { smsportal_order_sms( $order, 'completed', 'Order #{number} has shipped. Example Store' ); }, 10, 2 ); function smsportal_order_sms( $order, $status, $template ) { if ( ! defined( 'SMSPORTAL_API_KEY' ) ) { return; } $flag = '_smsportal_sms_' . $status; if ( $order->get_meta( $flag ) ) { return; // this text already went out, do not send it twice } // Your country code without the plus: 1 for US/Canada, 44 for the UK. $phone = smsportal_normalize_phone( $order->get_billing_phone(), '1' ); if ( '' === $phone ) { $order->add_order_note( 'SMS not sent: no valid phone number.' ); return; } $message = strtr( $template, array( '{number}' => $order->get_order_number(), '{name}' => $order->get_billing_first_name(), ) ); $response = wp_remote_post( 'https://smsportal.app/api/v1/gateway/send-sms', array( 'timeout' => 5, 'headers' => array( 'x-api-key' => SMSPORTAL_API_KEY, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'recipients' => array( $phone ), 'message' => $message, ) ), ) ); if ( is_wp_error( $response ) ) { $order->add_order_note( 'SMS not sent: ' . $response->get_error_message() ); return; } $code = (int) wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); if ( $code < 200 || $code >= 300 ) { $order->add_order_note( sprintf( 'SMS rejected (HTTP %d): %s', $code, substr( $body, 0, 200 ) ) ); return; } $order->update_meta_data( $flag, current_time( 'mysql' ) ); $order->save_meta_data(); $json = json_decode( $body, true ); $batch = isset( $json['data']['smsBatchId'] ) ? ' (batch ' . $json['data']['smsBatchId'] . ')' : ''; $order->add_order_note( 'SMS queued to ' . $phone . $batch . ': ' . $message ); } // (415) 555-0123 -> +14155550123. Numbers starting with + or 00 are kept. function smsportal_normalize_phone( $raw, $country_code ) { $raw = trim( (string) $raw ); $digits = preg_replace( '/\D+/', '', $raw ); if ( '' === $digits ) { return ''; } if ( 0 === strpos( $raw, '+' ) ) { // already international } elseif ( 0 === strpos( $digits, '00' ) ) { $digits = substr( $digits, 2 ); } elseif ( ! ( 0 === strpos( $digits, $country_code ) && strlen( $digits ) > 10 ) ) { $digits = $country_code . ltrim( $digits, '0' ); } $length = strlen( $digits ); return ( $length >= 8 && $length <= 15 ) ? '+' . $digits : ''; }Choose statuses and texts
Each status is its own hook,
woocommerce_order_status_<status>(slug without thewc-prefix). The templates support{number}and{name}. Keep them under 160 plain characters so each one is a single SMS part.Test with your own number
Place an order with your phone typed the way customers do, without the country code, then change its status in the admin. The order notes show "SMS queued" or the reason it failed. Setting the same status again does not send a second text.
Ready-made scenarios
Order shipped, with tracking number
The
woocommerce_order_status_completedhook sends "Order has shipped". Tracking is not a core WooCommerce field: read it with$order->get_meta()from the field your tracking plugin writes, and add it tostrtras{tracking}.Failed payment
Add a third hook,
woocommerce_order_status_failed, with a text like "Payment for #{number} did not go through, you can finish it in the store". The link from$order->get_checkout_payment_url()is long, so that SMS will take several parts.Ready for pickup
A "Ready for pickup" status added by a plugin has its own slug, for example
ready-pickup. The hook is thenwoocommerce_order_status_ready-pickup, and the text gives the pickup hours.
Errors and fixes
- Order note "SMS rejected (HTTP 401)"
- Cause: A wrong key, or an
Authorizationheader instead ofx-api-key. - Fix: Check the
SMSPORTAL_API_KEYconstant inwp-config.phpand generate a new key if the old one was deleted. - Order note "SMS rejected (HTTP 400)"
- Cause: No enabled phone on the account, or the account email is not verified.
- Fix: In the dashboard check that the phone is connected and enabled, and confirm your email address.
- Order note "SMS rejected (HTTP 429)"
- Cause: The SMS limit of your plan is used up.
- Fix: The response text in the note says which limit ran out. Change plans or wait for the allowance to renew.
- No SMS note at all
- Cause: The
SMSPORTAL_API_KEYconstant is missing, the snippet is inactive, or the status has no hook. - Fix: Check that the snippet is active and that the constant is defined before
require_once ABSPATH . 'wp-settings.php'. - Note says "SMS queued" but nothing arrives
- Cause: The phone is offline or Android puts the app to sleep.
- Fix: Check signal, internet and the phone status in the dashboard. Queued messages wait up to 72 hours, then expire.
Frequently asked questions
Does this work with HPOS (High-Performance Order Storage)?
Yes. The status hooks are fired by the order object whatever the storage, and the sent flag is saved with `update_meta_data` and `save_meta_data`, the methods WooCommerce recommends for HPOS instead of `update_post_meta`.
Does sending slow down checkout?
The request waits at most 5 seconds for a reply, usually far less. `'blocking' => false` makes it instant, but then the code never sees the result, so it cannot write the order note or the flag that prevents duplicates.
Can I use WooCommerce webhooks instead of code?
Yes, but a webhook from WooCommerce → Settings → Advanced → Webhooks sends the WooCommerce order format, which smsportal does not accept. You need a middle step such as n8n or Make that picks out the phone number and builds the text.
Where do customer replies go?
Into the smsportal dashboard, because the customer replies to your number. To forward them, for example to Slack, set up a webhook on `MESSAGE_RECEIVED` in n8n or Make.
Do I need consent for order texts?
A message about an order the customer placed is transactional. Promotions, coupons or ads in a text need separate marketing consent under the rules where you sell. For campaigns, use a provider with a sender name.
Sources
- WooCommerce: WC_Order source (status hooks)
- WooCommerce: HPOS recipe book (order metadata)
- WooCommerce: Webhooks
- WordPress: WP_Http::request (timeout, blocking)
- Code Snippets in the WordPress plugin directory
- smsportal API documentation
More in the guides
Send your first SMS today
Create a free account, connect your phone and see how customers respond to messages from your number.