API

All Goodtill stores have access to our API which can be used to:

Documentation for the API endpoints is available at https://apidoc.thegoodtill.com

Test accounts

If you are building an integration and you would like to test the API without affecting your live account, or you are building an integration on behalf of a Goodtill customer, you request a fully-featured test account from support@thegoodtill.com. Just include some details about the integration and which customer the integration is for.

Authentication

Authentication is handled by making a login call with credentials (subdomain, username and password) for an administrator or store owner user within the Goodtill store. This response will contain a JSON Web Token (JWT) which can be used to authorize future requests by passing it in the Authorization header (eg "Authorization: Bearer token_here").

Tokens are short lived and must be refreshed to keep them active.

Certain endpoints require an extra Vendor-Id token – please request this token from support@thegoodtill.com if you require access to these endpoints.

Data format

Requests bodies may be encoded as JSON or x-www-form-encoded, however JSON should be used if possible.

Responses bodies are encoded as JSON.

Dates and times included in responses will be returned in the timezone configured in the Goodtill Store.

Outlets

Stores can be configured with multiple outlets, which allows segregating data such as sales, products and stock quantities in different locations. Data entities can be store-wide (such a customers), outlet specific (such as sales) or optionally store-wide (such as products, via the Shareable toggle). The endpoint documentation states whether the entity available via the endpoint is outlet specific or store wide.

By default, all requests will fetch data from the outlet where the user account was created. It is possible to access data across multiple outlets using a single user account by specifying an outlet ID in the request, eg "Outlet-Id: outlet_id_here". This will change the outlet where data is accessed from if the user has been granted access to the selected outlet (eg via outlet tagging or when using the store owner account which has access to all outlets). A list of outlets (including IDs) that the user has access to can be obtained via the outlets endpoint.

Rate Limiting

The API endpoints are not currently rate limited, however this is subject to change.

Webhooks

Your integration can get near real-time notifications of certain events in your Goodtill store, such as new sales, using webhooks.

Terms of use

All integrations must follow our terms of use

Contacting customers

If you accessing customer data via the API for marketing purposes (eg to contact the customer via email, phone, post etc) you must exclude customers who have opted out. You should check the customer's opt_in_email setting at a regular basis. If this is false, the customer must not be contacted for marketing reasons (eg to notify them of promotions, request feedback etc).

Support

If you have any questions, please contact dev@thegoodtill.com

Client libraries

The following client libraries can be used to connect to the Goodtill API. For support, please contact the developer of the library.

Example Code

The following is a minimal example of how you can fetch data using the Goodtill API using PHP.

The code does the following:

  • Obtain a JWT for accessing the Goodtill API.
  • Fetches the sales for a date range using multiple requests (due to pagination).
  • Export the list of sales items in the sale as a CSV.

You will need to enter the credentials for an admin or store owner and account and your verification token in the define() calls.

In order to keep this example simple, we will call the POST api/login endpoint each time this script is called. Ideally you would save the token in your database allowing you to reuse it until it needs to be refreshed.

<?php

// This script requires PHP >=7.1

// Set Goodtill credentials
define('GOODTILL_SUBDOMAIN', '');
define('GOODTILL_USERNAME', '');
define('GOODTILL_PASSWORD', '');

class GoodtillException extends \Exception { }

class GoodtillClient {

public $token = null;
public $outletId = null;
public $vendorId = null;

public function request(string $url, string $method='GET', array $data=[]): array
{
$method = strtoupper($method);
$request_url = 'https://api.thegoodtill.com/api/'.$url;

if ($method == 'GET')
$request_url .= '?'.http_build_query($data);

$headers = [
'Content-type: application/json',
'User-Agent: GoodtillPhpClient (+https://support.thegoodtill.com/support/api/)',
];
if ($this->token)
$headers[] = 'Authorization: Bearer '.$this->token;
if ($this->outletId)
$headers[] = 'Outlet-Id: '.$this->outletId;
if ($this->vendorId)
$headers[] = 'Vendor-Id: '.$this->vendorId;

// Make request
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $request_url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
if ($method != 'GET'){
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
}
$responseBody = curl_exec($curl);
$responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$error = curl_error($curl);
curl_close($curl);

$parsedBody = json_decode($responseBody, true);

// Check for errors
if (empty($error) == false)
throw new GoodtillException($error);

if ($responseCode > 400){
$errorMessage = $parsedBody['message'] ?? $parsedBody['error'] ?? $responseBody;
throw new GoodtillException($errorMessage, $responseCode);
}

// Return data
return $parsedBody;
}

public function login(string $subdomain, string $username, string $password): array
{
$data = $this->request(
'login',
'POST',
[
'subdomain' => $subdomain,
'username' => $username,
'password' => $password,
]
);

$this->token = $data['token'];

return $data;
}
}

try {
// Login
$goodtill = new GoodtillClient();
$goodtill->login(GOODTILL_SUBDOMAIN, GOODTILL_USERNAME, GOODTILL_PASSWORD);

// Fetch the sales each page at a time and stop when we get to the end (no more sales returned).
$sales = [];
$limit = 50;
$offset = 0;
while (true) {
$response = $goodtill->request('external/get_sales_details', 'GET', [
'timezone' => 'local',
'from' => '2019-01-01',
'to' => '2019-01-08',
'limit' => $limit,
'offset' => $offset,
]);

if (empty($response['data']))
break;

$sales = array_merge($sales, $response['data']);

$offset += $limit;
}

// Format the retrieved data as a CSV
$handle = fopen('php://memory', 'rw+');
fputcsv($handle, ['product_id', 'product_name', 'quantity', 'line_total_after_discount']);

foreach ($sales as $sale){
foreach ($sale['sales_details']['sales_items'] as $sale_item){
fputcsv($handle, [
'product_id' => $sale_item['product_id'],
'product_name' => $sale_item['product_name'],
'quantity' => $sale_item['quantity'],
'line_total_after_discount' => $sale_item['line_total_after_line_discount'],
]);
}
}

// Get CSV from stream
fseek($handle, 0);
$csv = stream_get_contents($handle);
fclose($handle);
echo $csv;

} catch (GoodtillException $e) {
echo 'Goodtill API exception: '.$e->getMessage();
} catch (\Exception $e) {
echo 'Generic exception: '.$e->getMessage();
}

Example output:

product_id,product_name,quantity,line_total_after_discount
50b2bd53-c0fe-43ca-a16e-f819be792421,Macchiato,1,2.00
8664c2e7-ecba-451c-a9f2-8f06790bcaf3,Cortado,1,2.20
a4ad8d8a-0609-4741-b21f-354a3d896a42,Americano,1,2.80
...