[#1275] Unable to Authenticate with SimplyBook.me API – Consistent 401 Errors Despite Following Documentation

Migrated from Redmine #1275 | Author: Brandon Truong
Status: New | Priority: High, I’m very impatient | Created: 2024-09-06


I am experiencing consistent 401 Unauthorized errors when trying to connect to the SimplyBook.me API, despite following the documentation and implementing proper authentication headers. I’m attempting to create a booking.

I have tried connecting using both Postman and cURL with my API key. My request includes the X-Company-Login and X-Token headers as per the documentation, yet I still receive a 401 error.

I actually, initially tried integrating the API with custom PHP and JS on my client’s WordPress site but faced the same issue. I then turned to Postman and cURL to isolate and verify the problem, but the authentication issue persists across all attempts.

I’ve been at this for 3 days and I’m starting to pull the hairs off my head.

Could you please assist with identifying why the API is rejecting my requests.

Below, is the response I get:
{
“code”: 401,
“message”: “Unauthorized”,
“data”: ,
“message_data”:
}

My testing link with my PHP and JS implemented in trying to create a booking:

I also receive a 401 error here as well when viewing the response in the console.

For good measure here is my PHP:
add_action(‘wp_ajax_send_booking_data’, ‘send_booking_data’);
add_action(‘wp_ajax_nopriv_send_booking_data’, ‘send_booking_data’);

function send_booking_data() {
if (!defined(‘DOING_AJAX’) || !DOING_AJAX) {
wp_send_json_error(‘Not a valid AJAX request.’);
wp_die();
}

$name = sanitize_text_field($_POST['name']);
$email = sanitize_email($_POST['email']);
$phone = sanitize_text_field($_POST['phone']);
$message = sanitize_textarea_field($_POST['message']);
$service_id = sanitize_text_field($_POST['service_id']);
$date = sanitize_text_field($_POST['date']);
$time = sanitize_text_field($_POST['time']);

$start_datetime = $date . 'T' . $time . ':00Z';

$api_token = 'b76bbe238e7ee0952f4c83d13e1bbc9f5ec878ff213a7190416757f3e4ff2712';
$company_login = 'purelivingcleaners';

$booking_data = array(
    'service_id' => $service_id,
    'start_datetime' => $start_datetime,
    'client' => array(
        'name'  => $name,
        'email' => $email,
        'phone' => $phone,
    ),
    'comment' => $message,
);

$response = wp_remote_post('https://user-api-v2.simplybook.me/admin/bookings', array(
    'method'    => 'POST',
    'headers'   => array(
        'Content-Type'  => 'application/json',
        'X-Company-Login' => $company_login,
        'X-Token'       => $api_token,
    ),
    'body'      => json_encode($booking_data),
));

if (is_wp_error($response)) {
    wp_send_json_error('Failed to connect to SimplyBook.me: ' . $response->get_error_message());
    wp_die();
}

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

error_log('SimplyBook.me response: ' . print_r($decoded_response, true));

if (isset($decoded_response['error'])) {
    wp_send_json_error('SimplyBook API error: ' . $decoded_response['error']['message']);
    wp_die();
}

wp_send_json_success('Booking created successfully with SimplyBook.me! Full response: ' . json_encode($decoded_response));
wp_die();

}

and my JS:
document.addEventListener(“DOMContentLoaded”, function() {
if (document.body.classList.contains(‘single-ql_services’) || document.body.classList.contains(‘home’)) {
var submitButton = document.querySelector(‘.custom-submit-button’);

    if (submitButton) {
        submitButton.addEventListener("click", function(event) {
            event.preventDefault();

            var formData = new FormData();
            formData.append('action', 'send_booking_data');
            formData.append('name', document.querySelector('input[name="name"]').value);
            formData.append('email', document.querySelector('input[name="email"]').value);
            formData.append('phone', document.querySelector('input[name="phone"]').value);
            formData.append('message', document.querySelector('textarea[name="message"]').value);

            var serviceId = document.querySelector('input[name="service_id"]').value;
            formData.append('service_id', serviceId);

            var date = document.querySelector('#booking_date').value;
            var time = document.querySelector('#booking_time').value;

            if (date && time) {
                formData.append('date', date);
                formData.append('time', time);
            } else {
                console.error('Date or time not set');
                return;
            }

            fetch(ajax_object.ajaxurl, {
                method: 'POST',
                body: formData
            })
            .then(response => {
                if (!response.ok) {
                    throw new Error('Network response was not ok. Status: ' + response.status);
                }
                return response.json();
            })
            .then(data => {
                if (data.success) {
                    console.log('Booking successful:', data);
                } else {
                    console.error('Booking failed:', data);
                }
            })
            .catch(error => {
                console.error('Booking failed:', error);
            });
        });
    }
}

});

Redmine Admin wrote:

hi, please provide raw http request

Brandon Truong wrote:

curl --location ‘https://user-api-v2.simplybook.me/admin/bookings
–header ‘X-Company-Login: purelivingcleaners’
–header ‘X-API-KEY: b76bbe238e7ee0952f4c83d13e1bbc9f5ec878ff213a7190416757f3e4ff2712’
–header ‘Content-Type: application/json’
–data-raw '{
“provider_id”: 2,
“service_id”: 2,
“start_datetime”: “2024-09-10T12:00:00”,
“client”: {
“name”: “John Doe”,
“email”: “johndoe@example.com”,
“phone”: “+1234567890”
},
“custom”: {
“field_1”: “custom_value”
}
}

Brandon Truong wrote:

The last message is the raw http request from postman

and this one here is from the WordPress site’s submit form (to create a new booking):
POST /wp-admin/admin-ajax.php HTTP/2
Host: purelivingcleaners.ca
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:129.0) Gecko/20100101 Firefox/129.0
Accept: /
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Referer: https://purelivingcleaners.ca/services/residential-cleaning/
Content-Type: multipart/form-data; boundary=---------------------------348284750356535963373899578
Content-Length: 991
Origin: https://purelivingcleaners.ca
Connection: keep-alive
Cookie: wordpress_sec_3325fa862ab4956fe3e315ecb804fd53=plc_admin%7C1725807387%7CJAUPguZANemALp16TVXZC0fsee8vBbhG1t8hEOanL78%7C73451e4fe45ae95bd4ebf3c178389c847b8a7f080d733054a225152dd05773c4; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_3325fa862ab4956fe3e315ecb804fd53=plc_admin%7C1725807387%7CJAUPguZANemALp16TVXZC0fsee8vBbhG1t8hEOanL78%7C8e05e84e69eecfbe12f051f7e002da41ae2e278cc8a50d4c3eee67c21d245a20; wp_lang=en_US; _lscache_vary=357b5e87ae30c685f8fe139cda27f2ba
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Sec-GPC: 1
Priority: u=0
TE: trailers

Dmytro Bondarev wrote:

Hi, You should login and then pass x-token in headers.
Please check documentation.