Warm tip: This article is reproduced from serverfault.com, please click

How to Submit Post Requests to Xero API Inside Xero Webhook

发布于 2020-12-01 12:29:32

I have a Webhook on my Xero Account for Invoices. When a new invoice is created I want to access the Xero Api using the data from the Webhook to Send the user the invoice email. I have all the code for this but my issue is that the Webhook expects a response of 200 and when I call the Xero Api code from within the webhook I get an error on Xero saying the response contained a body. This makes sense as I'm submitting and getting data from the Api.

So how can I make my requests to the Xero Api without interfering with the Xero Webhook response?

Webhook Code:

<?php
    ///////////////////////////////////////////////////////////////////////////
    // WEBHOOK AUTHENTICATION - START
    ///////////////////////////////////////////////////////////////////////////
    //hook section
    $rawPayload = file_get_contents("php://input");
    // Update your webhooks key here
    $webhookKey = 'myWebhookKey';

    // Compute the payload with HMACSHA256 with base64 encoding
    $computedSignatureKey = base64_encode(
        hash_hmac('sha256', $rawPayload, $webhookKey, true)
    );

    // Signature key from Xero request
    $xeroSignatureKey = $_SERVER['HTTP_X_XERO_SIGNATURE'];

    $isEqual = false;

    if (hash_equals($computedSignatureKey, $xeroSignatureKey)) {
        $isEqual = true;
        http_response_code(200);
        
        // getting and passing the data to the api functionality
        $data = json_decode($rawPayload);
        xero_api($data);
    } else {
        http_response_code(401);
    }
    ///////////////////////////////////////////////////////////////////////////
    // WEBHOOK AUTHENTICATION - END
    ///////////////////////////////////////////////////////////////////////////
?>

Api code:

<?php
///////////////////////////////////////////////////////////////////////////
// XERO API FUNCITONALITY - START
///////////////////////////////////////////////////////////////////////////
function xero_api($data) {
    if ($data->events[0]->eventType === 'CREATE') {
        $resourseId = $data->events[0]->resourceId;

        ///////////////////////////////////////////////////////////////////////////
        // GET XERO CREDENTIALS - START
        ///////////////////////////////////////////////////////////////////////////
        global $wpdb;

        $xeroKeys = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}xero_keys WHERE ID = 0", ARRAY_A);

        $clientId = $xeroKeys['client_id'];
        $clientSecret = $xeroKeys['client_secret'];
        $refreshToken = $xeroKeys['refresh_token'];
        $tenantId = $xeroKeys['tenant_id'];
        ///////////////////////////////////////////////////////////////////////////
        // GET XERO CREDENTIALS - END
        ///////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////
        // GET ACCESS TOKEN AND GENERATE NEW REFRESH TOKEN - START
        ///////////////////////////////////////////////////////////////////////////
        $args = array(
            'headers' => array(
                'grant_type' => 'refresh_token',
            ),
            'body' => array(
                'grant_type' => 'refresh_token',
                'refresh_token' => $refreshToken,
                'client_id' => $clientId,
                'client_secret' => $clientSecret
            )
        );

        $refreshTokenRes = wp_remote_post('https://identity.xero.com/connect/token?=', $args);
        $refreshTokenBody = json_decode($refreshTokenRes['body']);

        if (isset($refreshTokenBody->refresh_token) && isset($refreshTokenBody->access_token)) {
            $updateTokens = $wpdb->update(
                $wpdb->prefix . 'xero_keys',
                array(
                    'refresh_token' => $refreshTokenBody->refresh_token,
                    'access_token' => $refreshTokenBody->access_token
                ),
                array('ID' => 0),
                array('%s', '%s'),
                array('%d')
            );
        }
        ///////////////////////////////////////////////////////////////////////////
        // GET ACCESS TOKEN AND GENERATE NEW REFRESH TOKEN - End
        ///////////////////////////////////////////////////////////////////////////

        $args = array(
            'headers' => array(
                'xero-tenant-id' => $tenantId,
                'Authorization' => 'Bearer ' . $refreshTokenBody->access_token,
                'Accept' => 'application/json',
                'Content-Type' => 'application/json'
            ),
        ); 

        $response = wp_remote_post('https://api.xero.com/api.xro/2.0/Invoices/' . $resourseId . '/Email', $args);
    }
}
///////////////////////////////////////////////////////////////////////////
// XERO API FUNCITONALITY - END
///////////////////////////////////////////////////////////////////////////
?>
Questioner
Reece
Viewed
0
dingo_d 2020-12-06 18:23:20

The problem is that you need to separate these calls.

As the comment to your question said, you need to first deal with the webhook, then queue the job that will trigger your API. Something like this would do

<?php
    ///////////////////////////////////////////////////////////////////////////
    // WEBHOOK AUTHENTICATION - START
    ///////////////////////////////////////////////////////////////////////////
    //hook section
    $rawPayload = file_get_contents("php://input");
    // Update your webhooks key here
    $webhookKey = 'myWebhookKey';

    // Compute the payload with HMACSHA256 with base64 encoding
    $computedSignatureKey = base64_encode(
        hash_hmac('sha256', $rawPayload, $webhookKey, true)
    );

    // Signature key from Xero request
    $xeroSignatureKey = $_SERVER['HTTP_X_XERO_SIGNATURE'];

    $isEqual = false;

    if (hash_equals($computedSignatureKey, $xeroSignatureKey)) {
        $isEqual = true;
        http_response_code(200);
        
        // getting and passing the data to the api functionality
        $data = json_decode($rawPayload);

        wp_schedule_single_event(
            time() + 10,
            'send_xero_api_call',
            ['data' => $data]
        );
    } else {
        http_response_code(401);
    }
    ///////////////////////////////////////////////////////////////////////////
    // WEBHOOK AUTHENTICATION - END
    ///////////////////////////////////////////////////////////////////////////

Then you need to register a WP Cron

add_action('send_xero_api_call', 'xero_api_cron', 10);

function xero_api_cron($data) {
    if ($data->events[0]->eventType === 'CREATE') {
        $resourseId = $data->events[0]->resourceId;

        ///////////////////////////////////////////////////////////////////////////
        // GET XERO CREDENTIALS - START
        ///////////////////////////////////////////////////////////////////////////
        global $wpdb;

        $xeroKeys = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}xero_keys WHERE ID = 0", ARRAY_A);

        $clientId = $xeroKeys['client_id'];
        $clientSecret = $xeroKeys['client_secret'];
        $refreshToken = $xeroKeys['refresh_token'];
        $tenantId = $xeroKeys['tenant_id'];
        ///////////////////////////////////////////////////////////////////////////
        // GET XERO CREDENTIALS - END
        ///////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////
        // GET ACCESS TOKEN AND GENERATE NEW REFRESH TOKEN - START
        ///////////////////////////////////////////////////////////////////////////
        $args = array(
            'headers' => array(
                'grant_type' => 'refresh_token',
            ),
            'body' => array(
                'grant_type' => 'refresh_token',
                'refresh_token' => $refreshToken,
                'client_id' => $clientId,
                'client_secret' => $clientSecret
            )
        );

        $refreshTokenRes = wp_remote_post('https://identity.xero.com/connect/token?=', $args);
        $refreshTokenBody = json_decode($refreshTokenRes['body']);

        if (isset($refreshTokenBody->refresh_token) && isset($refreshTokenBody->access_token)) {
            $updateTokens = $wpdb->update(
                $wpdb->prefix . 'xero_keys',
                array(
                    'refresh_token' => $refreshTokenBody->refresh_token,
                    'access_token' => $refreshTokenBody->access_token
                ),
                array('ID' => 0),
                array('%s', '%s'),
                array('%d')
            );
        }
        ///////////////////////////////////////////////////////////////////////////
        // GET ACCESS TOKEN AND GENERATE NEW REFRESH TOKEN - End
        ///////////////////////////////////////////////////////////////////////////

        $args = array(
            'headers' => array(
                'xero-tenant-id' => $tenantId,
                'Authorization' => 'Bearer ' . $refreshTokenBody->access_token,
                'Accept' => 'application/json',
                'Content-Type' => 'application/json'
            ),
        ); 

        $response = wp_remote_post('https://api.xero.com/api.xro/2.0/Invoices/' . $resourseId . '/Email', $args);
    }
}

Something to this extent. The $data argument should contain the data you passed (not 100% sure how it looks for you). Also, you'll need to check the throttling by the API, so you might adjust the time of the execution of your job. And make sure you have somewhere stored if your background job finished successfully.

One thing I suggest, since you are storing sensitive info in your DB (access and refresh tokens), is to encrypt them when storing them in the DB, and decrypt when fetching them.

You can check how I implemented background jobs in my plugin

https://github.com/dingo-d/woo-solo-api/tree/develop/src/BackgroundJobs https://github.com/dingo-d/woo-solo-api/blob/develop/src/Request/SoloApiRequest.php#L428-L438

You can use WP Queue lib from deliciousbrains: https://github.com/deliciousbrains/wp-queue/

It's a wrapper around WP Cron with custom DB tables for handling the queues, which will allow you to check if the jobs executed correctly or not.