Following a question in the WP Business Builders Facebook page about how to a) collect checkout data, b) format it and store it as user meta, I thought I would write a quick article to cover this and pass on a way to do this with nothing but a bunch of snippets. No automation tool at all, thanks to SureCarts customisation and the way it presents data and the fact that it is WordPress first, this is actually quite easy.
To begin with, we need to add a field to our checkout. In my case, this will be birthday as this was the request, and it allows the requester to just copy and paste the bits they want.
Checkout Customisation
So first we need to customise our checkout. In my case, I have done the main checkout, but you could also do a checkout form, and we need to add a new text field to the checkout. The one in red is what I have used here

Once i’ve added it in my example and in the code, I have set it up to be called dob. My exact settings are below for you to see everything I am doing to set this up

This is what our new field looks like on the front end ready for our customers to begin entering it

And with that, our checkout is customised and ready to begin collecting this data. You will see I’ve made it a required field, meaning it is required to check out the cart / make the purchase.
Our backend integration
So in my backend, I will be using WPCodeBox, but equally, you could use functions.php or the excellent Fluent Snippets. This code does a lot, and actually, a lot of it is optional. Just getting the data in the user meta is really easy now if you don’t add the DOB normalise blocks. You should just write what is entered, but we are going to do some more with this, including allowing us to see and edit it in the admin area and validating it as valid on checkout.
Simple Integration with Normalisation
So Surecart has a handler called checkout confirmed I have already recorded a video and an article on checkout confirmed in Surecart and the data avaliable but this is what we are going to use in this example, and we are going to normalise the data in the database to the ISO classification.
<?php
/**
* Convert common DOB formats to YYYY-MM-DD.
* Supports:
* - YYYY-MM-DD
* - DD/MM/YYYY
* - DD/MM/YY (00-69 => 2000-2069, 70-99 => 1970-1999)
*/
function sc_normalize_dob_to_ymd( $value ) {
$value = trim( (string) $value );
if ( $value === '' ) return '';
// Already correct
if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
return $value;
}
// DD/MM/YYYY or DD/MM/YY
if ( preg_match( '/^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})$/', $value, $m ) ) {
$day = (int) $m[1];
$month = (int) $m[2];
$year = (int) $m[3];
if ( $year < 100 ) {
$year = ( $year <= 69 ) ? ( 2000 + $year ) : ( 1900 + $year );
}
if ( checkdate( $month, $day, $year ) ) {
return sprintf( '%04d-%02d-%02d', $year, $month, $day );
}
}
// Last resort
$ts = strtotime( $value );
return $ts ? gmdate( 'Y-m-d', $ts ) : '';
}
/**
* A) SureCart: On checkout confirmed, save DOB to WP usermeta (normalized).
*/
add_action( 'surecart/checkout_confirmed', function( $checkout, $request ) {
// Pull metadata from checkout first, then fallback to request.
$metadata = [];
if ( isset( $checkout->metadata ) ) {
$metadata = is_array( $checkout->metadata ) ? $checkout->metadata : (array) $checkout->metadata;
}
if ( empty( $metadata ) && $request instanceof \WP_REST_Request ) {
$req_meta = $request->get_param( 'metadata' );
if ( is_array( $req_meta ) ) {
$metadata = $req_meta;
}
}
$dob_raw = $metadata['dob'] ?? '';
$dob_raw = is_string( $dob_raw ) ? trim( $dob_raw ) : '';
if ( $dob_raw === '' ) return;
$dob_ymd = sc_normalize_dob_to_ymd( $dob_raw );
if ( $dob_ymd === '' ) return;
// Find WP user by email (match SureCart customer email).
$email = '';
if ( isset( $checkout->customer ) && is_object( $checkout->customer ) && ! empty( $checkout->customer->email ) ) {
$email = trim( (string) $checkout->customer->email );
} elseif ( $request instanceof \WP_REST_Request ) {
$email = trim( (string) $request->get_param( 'email' ) );
}
if ( $email === '' ) return;
$user = get_user_by( 'email', $email );
if ( ! $user || empty( $user->ID ) ) return;
update_user_meta( $user->ID, 'dob', $dob_ymd );
}, 10, 2 );PHPThis will look for our dob field, it will add it to the usermeta on checkout, and update it if needed. What it won’t allow us to do is validate it or see it in the admin area, and we will focus on seeing it first.
Viewing the new meta in the user profile
The following snippet adds the new meta to the WordPress admin and allows us to edit it within the edit user interface
/**
* B) Show DOB in wp-admin when editing a user.
*/
function sc_add_dob_user_profile_field( $user ) {
$raw = get_user_meta( $user->ID, 'dob', true );
$dob_for_input = sc_normalize_dob_to_ymd( $raw );
?>
<h2>SureCart</h2>
<table class="form-table" role="presentation">
<tr>
<th><label for="dob">Date of Birth</label></th>
<td>
<input
type="date"
name="dob"
id="dob"
value="<?php echo esc_attr( $dob_for_input ); ?>"
class="regular-text"
/>
<p class="description">
Usermeta key: <code>dob</code>.
Raw stored value: <code><?php echo esc_html( (string) $raw ); ?></code>
</p>
</td>
</tr>
</table>
<?php
}
add_action( 'show_user_profile', 'sc_add_dob_user_profile_field' );
add_action( 'edit_user_profile', 'sc_add_dob_user_profile_field' );
/**
* Save DOB when updated in wp-admin.
*/
function sc_save_dob_user_profile_field( $user_id ) {
if ( ! current_user_can( 'edit_user', $user_id ) ) return;
if ( ! isset( $_POST['dob'] ) ) return;
$dob_raw = sanitize_text_field( wp_unslash( $_POST['dob'] ) );
if ( $dob_raw === '' ) {
delete_user_meta( $user_id, 'dob' );
return;
}
$dob_ymd = sc_normalize_dob_to_ymd( $dob_raw );
if ( $dob_ymd !== '' ) {
update_user_meta( $user_id, 'dob', $dob_ymd );
}
}
add_action( 'personal_options_update', 'sc_save_dob_user_profile_field' );
add_action( 'edit_user_profile_update', 'sc_save_dob_user_profile_field' );
PHPWith this added, we can now see this in edit user and save the changes that we make here

We can go a step further, thanks to SureCart
Thanks to SureCarts’ Validate options, we can actually check our format and force a format too. In my example, I have used the UK, but equally, I’ve left it so that it can be changed to ISO or US mode too, to make it easier to reuse this code.
There is a really good example here for validating age, which I have replicated below. This isn’t part of the code needed for what we are doing, so don’t copy it to achieve the same results. It’s an example of something else you can do with validation.
add_filter( 'surecart/checkout/validate', function( $errors, $args, $request ) {
// Get custom fields from metadata
$metadata = $args['metadata'] ?? [];
// Validate age requirement
if ( isset( $metadata['your_age'] ) ) {
if ( $metadata['your_age'] < 18 ) {
$errors->add( 'age_restriction', 'You must be 18 or older to purchase.' );
}
}
return $errors;
}, 10, 3 );PHPSo we now know what we want to do, and this means adding the following code that works very similarly to the code above to our snippet. I have tried to cover as many situations here and to take control of that based on the perspective audiance free form text means dates like 04/03/2026 are hard to programmatically ascertain, and this is why I’ve added the HARD fall method to pick a format when this happens. This will depend on your target audience.
/**
* Parse DOB into [year, month, day] with preference handling.
* Accepts: YYYY-MM-DD, YYYY/MM/DD, DD/MM/YYYY, MM/DD/YYYY (also with '-')
* Requires: 4-digit year
*
* Returns [year, month, day] or null.
*/
function sc_parse_dob_with_preference( $dob_raw ) {
$dob_raw = trim( (string) $dob_raw );
if ( $dob_raw === '' ) return null;
// Normalize separator to '/'
$dob_norm = preg_replace( '/-/', '/', $dob_raw );
// ISO: YYYY/MM/DD (always unambiguous)
if ( preg_match( '/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/', $dob_norm, $m ) ) {
$y = (int) $m[1]; $mo = (int) $m[2]; $d = (int) $m[3];
return checkdate( $mo, $d, $y ) ? [ $y, $mo, $d ] : null;
}
// If ISO-only, stop here.
if ( defined( 'SC_DOB_PREFERENCE' ) && SC_DOB_PREFERENCE === 'ISO_ONLY' ) {
return null;
}
// D/M/Y or M/D/Y (ambiguous sometimes)
if ( preg_match( '/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/', $dob_norm, $m ) ) {
$a = (int) $m[1];
$b = (int) $m[2];
$y = (int) $m[3];
// Quick sanity
if ( $y < 1000 || $y > 9999 ) return null;
// If a > 12, must be DMY
if ( $a > 12 ) {
$d = $a; $mo = $b;
return checkdate( $mo, $d, $y ) ? [ $y, $mo, $d ] : null;
}
// If b > 12, must be MDY
if ( $b > 12 ) {
$mo = $a; $d = $b;
return checkdate( $mo, $d, $y ) ? [ $y, $mo, $d ] : null;
}
// Both <= 12 => ambiguous. Use preference.
if ( defined( 'SC_DOB_PREFERENCE' ) && SC_DOB_PREFERENCE === 'US' ) {
// Prefer MDY
$mo = $a; $d = $b;
if ( checkdate( $mo, $d, $y ) ) return [ $y, $mo, $d ];
// Fallback to DMY if somehow invalid
$d = $a; $mo = $b;
return checkdate( $mo, $d, $y ) ? [ $y, $mo, $d ] : null;
}
// Default UK preference: DMY
$d = $a; $mo = $b;
if ( checkdate( $mo, $d, $y ) ) return [ $y, $mo, $d ];
// Fallback to MDY if somehow invalid
$mo = $a; $d = $b;
return checkdate( $mo, $d, $y ) ? [ $y, $mo, $d ] : null;
}
return null;
}
/**
* SureCart checkout validation: flexible formats + 4-digit year.
* IMPORTANT: filter + return $errors to avoid JSON issues.
*/
add_filter( 'surecart/checkout/validate', function( $errors, $args, $request ) {
$metadata = $args['metadata'] ?? [];
$dob_raw = isset( $metadata['dob'] ) ? trim( (string) $metadata['dob'] ) : '';
// If required, uncomment:
// if ( $dob_raw === '' ) {
// $errors->add( 'dob_required', 'Date of Birth is required.' );
// return $errors;
// }
// If not required, only validate when present.
if ( $dob_raw === '' ) return $errors;
$parsed = sc_parse_dob_with_preference( $dob_raw );
if ( ! $parsed ) {
$msg = ( defined('SC_DOB_PREFERENCE') && SC_DOB_PREFERENCE === 'ISO_ONLY' )
? 'Please enter your Date of Birth as YYYY-MM-DD (4-digit year).'
: 'Please enter your Date of Birth as YYYY-MM-DD, DD/MM/YYYY, or MM/DD/YYYY (4-digit year).';
$errors->add( 'dob_format', $msg );
return $errors;
}
[ $year, $month, $day ] = $parsed;
// Not in the future
$today = (int) gmdate( 'Ymd' );
$dob_i = (int) sprintf( '%04d%02d%02d', $year, $month, $day );
if ( $dob_i > $today ) {
$errors->add( 'dob_future', 'Date of Birth cannot be in the future.' );
return $errors;
}
// Optional bounds
if ( $year < 1900 ) {
$errors->add( 'dob_year', 'Please enter a valid 4-digit year.' );
return $errors;
}
return $errors;
}, 10, 3 );PHPSo what does it look like when we can’t validate? We get our error message explaining the issue

And just like that, we’ve added a custom field, we’ve added it to the user meta, the admin can use it you could use it in other scripts to validate age-related items, and we did it all without any automation tool.
The Full Assembled working code from the below video is below
<?php
/**
* Convert common DOB formats to YYYY-MM-DD.
* Supports:
* - YYYY-MM-DD
* - DD/MM/YYYY
* - DD/MM/YY (00-69 => 2000-2069, 70-99 => 1970-1999)
*/
function sc_normalize_dob_to_ymd( $value ) {
$value = trim( (string) $value );
if ( $value === '' ) return '';
// Already correct
if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
return $value;
}
// DD/MM/YYYY or DD/MM/YY
if ( preg_match( '/^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})$/', $value, $m ) ) {
$day = (int) $m[1];
$month = (int) $m[2];
$year = (int) $m[3];
if ( $year < 100 ) {
$year = ( $year <= 69 ) ? ( 2000 + $year ) : ( 1900 + $year );
}
if ( checkdate( $month, $day, $year ) ) {
return sprintf( '%04d-%02d-%02d', $year, $month, $day );
}
}
// Last resort
$ts = strtotime( $value );
return $ts ? gmdate( 'Y-m-d', $ts ) : '';
}
/**
* A) SureCart: On checkout confirmed, save DOB to WP usermeta (normalized).
*/
add_action( 'surecart/checkout_confirmed', function( $checkout, $request ) {
// Pull metadata from checkout first, then fallback to request.
$metadata = [];
if ( isset( $checkout->metadata ) ) {
$metadata = is_array( $checkout->metadata ) ? $checkout->metadata : (array) $checkout->metadata;
}
if ( empty( $metadata ) && $request instanceof \WP_REST_Request ) {
$req_meta = $request->get_param( 'metadata' );
if ( is_array( $req_meta ) ) {
$metadata = $req_meta;
}
}
$dob_raw = $metadata['dob'] ?? '';
$dob_raw = is_string( $dob_raw ) ? trim( $dob_raw ) : '';
if ( $dob_raw === '' ) return;
$dob_ymd = sc_normalize_dob_to_ymd( $dob_raw );
if ( $dob_ymd === '' ) return;
// Find WP user by email (match SureCart customer email).
$email = '';
if ( isset( $checkout->customer ) && is_object( $checkout->customer ) && ! empty( $checkout->customer->email ) ) {
$email = trim( (string) $checkout->customer->email );
} elseif ( $request instanceof \WP_REST_Request ) {
$email = trim( (string) $request->get_param( 'email' ) );
}
if ( $email === '' ) return;
$user = get_user_by( 'email', $email );
if ( ! $user || empty( $user->ID ) ) return;
update_user_meta( $user->ID, 'dob', $dob_ymd );
}, 10, 2 );
/**
* B) Show DOB in wp-admin when editing a user.
*/
function sc_add_dob_user_profile_field( $user ) {
$raw = get_user_meta( $user->ID, 'dob', true );
$dob_for_input = sc_normalize_dob_to_ymd( $raw );
?>
<h2>SureCart</h2>
<table class="form-table" role="presentation">
<tr>
<th><label for="dob">Date of Birth</label></th>
<td>
<input
type="date"
name="dob"
id="dob"
value="<?php echo esc_attr( $dob_for_input ); ?>"
class="regular-text"
/>
<p class="description">
Usermeta key: <code>dob</code>.
Raw stored value: <code><?php echo esc_html( (string) $raw ); ?></code>
</p>
</td>
</tr>
</table>
<?php
}
add_action( 'show_user_profile', 'sc_add_dob_user_profile_field' );
add_action( 'edit_user_profile', 'sc_add_dob_user_profile_field' );
/**
* Save DOB when updated in wp-admin.
*/
function sc_save_dob_user_profile_field( $user_id ) {
if ( ! current_user_can( 'edit_user', $user_id ) ) return;
if ( ! isset( $_POST['dob'] ) ) return;
$dob_raw = sanitize_text_field( wp_unslash( $_POST['dob'] ) );
if ( $dob_raw === '' ) {
delete_user_meta( $user_id, 'dob' );
return;
}
$dob_ymd = sc_normalize_dob_to_ymd( $dob_raw );
if ( $dob_ymd !== '' ) {
update_user_meta( $user_id, 'dob', $dob_ymd );
}
}
add_action( 'personal_options_update', 'sc_save_dob_user_profile_field' );
add_action( 'edit_user_profile_update', 'sc_save_dob_user_profile_field' );
/**
* ===== DOB PARSING PREFERENCE =====
* 'UK' => prefer DD/MM/YYYY when ambiguous (e.g. 03/04/2025 => 3 April)
* 'US' => prefer MM/DD/YYYY when ambiguous (e.g. 03/04/2025 => March 4)
* 'ISO_ONLY' => only allow YYYY-MM-DD (or YYYY/MM/DD)
*/
define( 'SC_DOB_PREFERENCE', 'UK' ); // <-- change this to 'US' or 'ISO_ONLY'
/**
* Parse DOB into [year, month, day] with preference handling.
* Accepts: YYYY-MM-DD, YYYY/MM/DD, DD/MM/YYYY, MM/DD/YYYY (also with '-')
* Requires: 4-digit year
*
* Returns [year, month, day] or null.
*/
function sc_parse_dob_with_preference( $dob_raw ) {
$dob_raw = trim( (string) $dob_raw );
if ( $dob_raw === '' ) return null;
// Normalize separator to '/'
$dob_norm = preg_replace( '/-/', '/', $dob_raw );
// ISO: YYYY/MM/DD (always unambiguous)
if ( preg_match( '/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/', $dob_norm, $m ) ) {
$y = (int) $m[1]; $mo = (int) $m[2]; $d = (int) $m[3];
return checkdate( $mo, $d, $y ) ? [ $y, $mo, $d ] : null;
}
// If ISO-only, stop here.
if ( defined( 'SC_DOB_PREFERENCE' ) && SC_DOB_PREFERENCE === 'ISO_ONLY' ) {
return null;
}
// D/M/Y or M/D/Y (ambiguous sometimes)
if ( preg_match( '/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/', $dob_norm, $m ) ) {
$a = (int) $m[1];
$b = (int) $m[2];
$y = (int) $m[3];
// Quick sanity
if ( $y < 1000 || $y > 9999 ) return null;
// If a > 12, must be DMY
if ( $a > 12 ) {
$d = $a; $mo = $b;
return checkdate( $mo, $d, $y ) ? [ $y, $mo, $d ] : null;
}
// If b > 12, must be MDY
if ( $b > 12 ) {
$mo = $a; $d = $b;
return checkdate( $mo, $d, $y ) ? [ $y, $mo, $d ] : null;
}
// Both <= 12 => ambiguous. Use preference.
if ( defined( 'SC_DOB_PREFERENCE' ) && SC_DOB_PREFERENCE === 'US' ) {
// Prefer MDY
$mo = $a; $d = $b;
if ( checkdate( $mo, $d, $y ) ) return [ $y, $mo, $d ];
// Fallback to DMY if somehow invalid
$d = $a; $mo = $b;
return checkdate( $mo, $d, $y ) ? [ $y, $mo, $d ] : null;
}
// Default UK preference: DMY
$d = $a; $mo = $b;
if ( checkdate( $mo, $d, $y ) ) return [ $y, $mo, $d ];
// Fallback to MDY if somehow invalid
$mo = $a; $d = $b;
return checkdate( $mo, $d, $y ) ? [ $y, $mo, $d ] : null;
}
return null;
}
/**
* SureCart checkout validation: flexible formats + 4-digit year.
* IMPORTANT: filter + return $errors to avoid JSON issues.
*/
add_filter( 'surecart/checkout/validate', function( $errors, $args, $request ) {
$metadata = $args['metadata'] ?? [];
$dob_raw = isset( $metadata['dob'] ) ? trim( (string) $metadata['dob'] ) : '';
// If required, uncomment:
// if ( $dob_raw === '' ) {
// $errors->add( 'dob_required', 'Date of Birth is required.' );
// return $errors;
// }
// If not required, only validate when present.
if ( $dob_raw === '' ) return $errors;
$parsed = sc_parse_dob_with_preference( $dob_raw );
if ( ! $parsed ) {
$msg = ( defined('SC_DOB_PREFERENCE') && SC_DOB_PREFERENCE === 'ISO_ONLY' )
? 'Please enter your Date of Birth as YYYY-MM-DD (4-digit year).'
: 'Please enter your Date of Birth as YYYY-MM-DD, DD/MM/YYYY, or MM/DD/YYYY (4-digit year).';
$errors->add( 'dob_format', $msg );
return $errors;
}
[ $year, $month, $day ] = $parsed;
// Not in the future
$today = (int) gmdate( 'Ymd' );
$dob_i = (int) sprintf( '%04d%02d%02d', $year, $month, $day );
if ( $dob_i > $today ) {
$errors->add( 'dob_future', 'Date of Birth cannot be in the future.' );
return $errors;
}
// Optional bounds
if ( $year < 1900 ) {
$errors->add( 'dob_year', 'Please enter a valid 4-digit year.' );
return $errors;
}
return $errors;
}, 10, 3 );PHPWant to see it in action? Watch it here:
