Supa Integration Reference for POS systems (2.0)

License: MIT

About Supa

Supa is a QR payment solution for restaurants in Poland. It integrates with POS systems of restaurants and allows guests to see their orders, split them, pay for them & leave a review.

Why integrating with us?

Payment at the table is a new way of providing convenient service for the restaurant's guests.

Increase the restaurant turnover

  • speed up the payment process
  • achieve higher tips for waiters
  • Make your POS system providing modern features for your customers.

Setup & Integration

We do our best to make the integration between any POS system and Supa as easy as possible. So we created this guide to make it easier for you.

Step 1. Acquire a testing environment

Contact Supa team via the feedback form on our website: https://supa.pl/ We’ll provide you with a test restaurant for integration development and will stay in contact for any questions you have.

Once you receive a test restaurant from Supa technical team, you will see POS system integration section. You can find required information for integration in the POS restaurant account section:

  • Integration ID - unique identifier that will be generated for each restaurant you integrate with. Test restaurants have it - and all production restaurants will have it as well.
  • Webhook type - socket/https. A channel which will be used to dispatch events from Supa to your server. By default, “socket” is used - but it can be changed to https - just contact the support, provide webhook URL and we’ll change it for you
  • Adapter type - each integration action & response is mapped through the adapter. By default - the standard adapter is used (whatever goes in - goes out the same). Some of the commands may feature a complicated JSON data (for example, order details) - and Supa team may help you by mapping your custom data format to a required format.
  • Refresh token - this token encapsulates your integration ID and enables you to interact with Supa APIs.

account-info

Step 2. Authentication

We are using oAuth2 methods. For every Supa restaurant, a new unique refreshToken is generated. It has an infinite lifetime and may be used to acquire temporary authToken to access the API.

To get an auth token, please use AuthRefresh

A temporary token will be returned, that can be used to make HTTPS requests or create a socket.io connection.

Please note that if your integration for a specific restaurant will be revoked - refreshToken will no longer be able to generate you a fresh authToken.

Step 3. Connection

WebhookType “socket” (default)

Socket.io connection should be established between cash register server & Supa servers. Socket.io has client libraries built for all most common programming languages: Java, C++, Kotlin, C#, PHP etc.

  • URL: https://pos.supa.pl/socket.io
  • Transports: [websocket]
  • Connection string should include authToken as query parameter, for example:
https://pos.supa.pl/socket.io?authToken=eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...qg08a5iDZvMWDkUYJOAzs
  • The POS system has to connect to the Supa server as soon as it starts and make sure connection is established during the working hours of the restaurant.
  • Whenever Supa issues a command, it will use corresponding socket.io event (see details in commands section)

WebhookType "https"

You can request our support team to switch your webhook type to https.

  • Supa will call your desired webhook URL (it will be specified in the account details)
  • Supa will use Basic authentication with both “user” and “password” equal to your Integration ID.
  • Whenever Supa issues a command, a corresponding POST request will be issued (see details in commands section)

Step 4. Implement commands handling (Supa ➡️ calls ➡️ POS system)

In order to properly operate, Supa has to be in sync with the POS system and have the latest information about the restaurant and its orders. To achieve this, Supa will issue “commands” (either via socket.io or https) with specific for each command arguments.

Response to each command should be in format specified in this API reference, otherwise validation will fail and data will not be synced.

You can also return data in arbitrary format, but that will require a Supa team to create an adapter for your integration - which will require additional time & costs.

Currently, there are 8 commands that a POS system has to implement.

Please refer to the corresponding Commands Section

Step 5. Start sending events to Supa (POS system ➡️ calls ➡️ Supa)

Supa server has to be informed about a few events happening in your system. Regardless of the selected webhookType, you can always inform Supa either via:

Socket.IO

  • send an event to an existing socket connection
  • eventName should equal to commandName
  • args should be passed as first argument to the event
  • listen for an acknowledgement from Supa server to get a response.

For example:

socketClient.emit('onOrderPrint', { 
    externalOrderId: 'my-pos-order-id', 
    externalTableId: 'my-pos-table-id' 
},  (qrURL: string | null) => {
    if (typeof qrURL === 'string') {
        addQRCodeToBill({ text: qrURL })
    }
});

HTTPs

Call our https API: https://pos.supa.pl/integrations/third-party/{eventName}

  • add header Authorization: Bearer {authToken}
  • args should be added as json body

Currently, there are 2 required events that Supa server has to be notified of.

Please refer to the corresponding Events Section

Step 6. Error handling

Any webhook call (from Supa to the POS system) can produce an error.

  • For webhookType socketIO you can return event response with “error” field - and the response will be considered failed:
socketClient.on('getOrderById', (params, cb) => {
    try {
       // ... try to get an order by id 
    } catch (error) {
        cb({
          error: {
            message: error.message || "Oops, something went wrong",
            identifier: error.identifier || "OrderNotFoundException",
            payload: params
          }
        })
    }
});
  • webhookType https you can return a 4xx statusCode and the response will be considered failed. For example:
HTTP/1.1 404 Not Found
Content-Type: application/json

{
  statusCode: 404
  message: "Oops, something went wrong",
  identifier: "OrderNotFoundException",
  payload: { id: 123 }
}

Please inform the Supa team about possible errors your system could raise so we could properly inform your guests about actions they have to take in order to resolve the issues.

Each error is identified by "identifier" string that error object contains.

Currently, we support the following error identifiers:

  • "IntegrationEntityInUseException" - this will notify user that order is currently modified by the waiter
  • "IntegrationNotAvailableException" - this will notify user that POS is temporarily offline and he should try again shortly
  • "IntegrationAccountNotFoundException" - an integration/authentication problem occurred with the integration
  • "OrderSplitUnavailableException" - if user attempted to split, but it's not possible anymore
  • "OrderItemsNotFoundException" - if user attempted to split, but some items are no longer available
  • "OrderNotFoundException" - requested order was not found

Auth

AuthRefresh

query Parameters
refreshToken
required
string

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ],
  • "token": "string",
  • "refreshToken": "string",
  • "expiration": 0.1,
  • "entity": "string",
  • "resource": "string",
  • "payload": { }
}

ThirdPartyCommandsReference

ThirdPartyControllerPublicDocsOnCommandsReference

Webhooks to your POS system will be issues either through socket.io event or https endpoint.

Each command we issue has a corresponding args, and we expect from you a corresponding result

Each result returned from your POS system will be validated on our side, and we will display success or error for each operation in the Supa admin page.

Socket webhooks

For socket events:

  • commands will be executed as events with a corresponding name.
  • args object will be passed as a first and only event argument
  • args of type void will not be passed at all
  • POS system has to acknowledge the event (call a callback function with an appropriate response)

Your socket.io client should like in this example:

socketClient.on('getSections', (pagination, cb) => {
    const { page, limit } = pagination 

    const sections = [
        {
            externalId: 'section#1',
            name: 'VIP Section 1',
            description: 'Section for VIP Guests',
        },
    ];

    cb(sections.slice((page-1)*limit, page*limit));
});

For a full code reference for socket.io please take a look at Examples

HTTPS webhooks

For HTTPS webhooks we'll send a POST request with a corresponding JSON body. Example:

{ 
  command: 'getSections',  // getRestaurantAccount|getSections etc..
  args: { page: 1, limit: 15 } // or other arguments specific for other commands
}

Here in REQUEST BODY SCHEMA we're providing the models for the commands we'll issue through the webhooks.

  • Each key (getRestaurantAccount|getSections etc.) corresponds to a commandName.
  • Each args and result represent command details.
Request Body schema: application/json
required
required
object
required
object
required
object
required
object
required
object
required
object
required
object

This, in fact, is OPTIONAL.

Only required if you want to implement demo orders for your customers

required
object

This command may only be triggered for orders which POS system marked with splitAvailable=true

If your system does not support splitAvailable (it's false for all orders), you don't have to implement & support this command.

Note that this command will most of the time be followed by getOrderById for both original order (which has to be already split) & and the new order

required
object
required
object

Responses

Request samples

Content type
application/json
{
  • "getRestaurantAccount": {
    },
  • "getSections": {
    },
  • "getTables": {
    },
  • "getWaiters": {
    },
  • "getPaymentTypes": {
    },
  • "getOrderById": {
    },
  • "createOrder": {
    },
  • "splitOrder": {
    },
  • "billAndLockOrder": {
    },
  • "payOrder": {
    }
}

ThirdPartyExamples

ThirdPartyControllerPublicDocsExampleSocketOnSocketIOExample


import { io as SocketIoClient } from 'socket.io-client';
import axios from 'axios';
import dayjs from 'dayjs';

const BASE_URL = 'https://pos.supa.pl'
const REFRESH_TOKEN = 'eyJhbGciOiJIU...CqmpNPDvfPN4mJpiFJp_s';

const getAccessToken = async (refreshToken) => {
    const { data } = await axios.post(`${BASE_URL}/auth/refresh?refreshToken=${refreshToken}`);

    return data.token;
}

const paginate = (array, { page, limit }) => {
    return array.slice((page - 1) * limit, page * limit)
}

const authToken = await getAccessToken(REFRESH_TOKEN);

const socketClient = new SocketIoClient(BASE_URL, {
    path: '/socket.io',
    query: { authToken },
    transports: ['websocket'],
    secure: true,
});

socketClient.on('connect', async () => {
    console.log('POS connected');

    // socketClient.emit('onOrderPrint', { externalOrderId: 'order1', externalTableId: 'table1' },  (response) => console.log(response, ' | onOrderPrint response'));
    // socketClient.emit('PING', (response) => console.log(response, ' | PING response'));
    await new Promise(resolve => setTimeout(resolve, 1000));
    socketClient.emit('onCashRegisterStart');
    socketClient.emit('onOrderUpsert', { externalOrderId: 'order#1' });
});

socketClient.on('disconnect', () => {
    console.log('POS disconnected');
});

socketClient.on('connect_error', (error) => {
    if (socketClient.active) {
        // temporary failure, the socket will automatically try to reconnect
    } else {
        // the connection was denied by the server
        // in that case, `socket.connect()` must be manually called in order to reconnect
        console.log(error.message);
    }
});

socketClient.on('getRestaurantAccount', (cb) => {
    console.log('getRestaurantAccount');

    const account = {
        organisationId: 'orgId2',
        organisationName: 'Test Organisation',
        cloudId: '#001',
        tariff: 'Basic',
    };

    cb(account);
});

socketClient.on('getSections', (pagination, cb) => {
    console.log('getSections', pagination);

    const sections = [
        {
            externalId: 'section#1',
            name: 'VIP Section 1',
            description: 'Section for VIP Guests',
        },
    ];

    cb(paginate(sections, pagination));
});

socketClient.on('getWaiters', (pagination, cb) => {
    console.log('getWaiters', pagination);

    const waiters = [
        {
            'externalId': 'waiterExternalId#1',
            'name': 'Daniella Waiter',
            'phone': '+48835000000',
            'requiresPin': false
        }
    ];

    cb(paginate(waiters, pagination));
});

socketClient.on('getTables', (pagination, cb) => {
    console.log('getTables', pagination);

    const tables = [
        {
            externalId: 'table1#1',
            externalSectionId: 'section#1',
            name: 'Table 1',
            number: 1,
            description: 'VIP table',
        },
    ];

    cb(paginate(tables, pagination));
});

socketClient.on('getPaymentTypes', (pagination, cb) => {
    console.log('getPaymentTypes', pagination);

    const paymentTypes = [
        {
            externalId: 'paymentType#1',
            name: 'Supa',
            kind: 'card',
        },
    ];

    cb(paginate(paymentTypes, pagination));
});

socketClient.on('getOrderById', (params, cb) => {
    console.log('getOrderById', params);

    const ordersById = {
        'order#1': {
            status: 'opened',
            externalId: 'order#1',
            currency: 'PLN',
            sum: 12300,
            paidSum: 12300,
            fullSum: 12500,
            guestsCount: 2,

            openedAt: dayjs().subtract(2, 'hours').toISOString(),
            billedAt: dayjs().subtract(15, 'minutes').toISOString(),
            closedAt: null,

            guests: [
                {
                    externalId: 'your-guest-id-1',
                    name: 'Guest 1',
                    raw: {},
                },
                {
                    externalId: 'your-guest-id-2',
                    name: 'Guest 2',
                    raw: {},
                },
            ],

            items: [
                {
                    externalId: 'jsakjdsakj',
                    price: 5000,
                    name: 'Hamburger of 1st guest',
                    amount: 2,
                    cost: 10000,
                    comment: 'extra spicy',
                    guestExternalId: 'your-guest-id-1',
                    product: {
                        externalId: 'product-id#1',
                        price: 5000,
                        name: 'Hamburger of 1st guest',
                    },
                    modifiers: [
                        {
                            externalId: 'modifier#1',
                            productExternalId: 'modifier-type-id#1',
                            name: 'Additional Gouda',
                            amount: 2,
                            sum: 2000,
                        }
                    ],
                    raw: {},
                },
                {
                    externalId: 'jsakjdsakjвфіо213',
                    price: 5300,
                    name: 'Salad of 2nd guest',
                    amount: 2.5,
                    cost: 13250,
                    guestExternalId: 'your-guest-id-2',
                    product: {
                        externalId: 'product-id#2',
                        price: 5300,
                        name: 'Salad of 2nd guest',
                    },
                    raw: {},
                },
            ],

            payments: [
                {
                    externalId: 'payment-id',
                    sum: 12300,
                    status: 'processed',
                    currency: 'PLN',
                    restaurantPaymentType: { externalId: 'paymentType#1' },
                    raw: {},
                },
            ],
            tables: [{ externalId: 'table1#1' }],
            waiters: [{ externalId: 'waiterExternalId#1' }],

            raw: {}
        }
    }

    cb(ordersById[params.orderId]);
});

socketClient.on('billAndLockOrder', async (data, cb) => {
    console.log('billAndLockOrder', data);
    await new Promise(resolve => setTimeout(resolve, 1000));

    cb();
});

socketClient.on('payOrder', async (data, cb) => {
    console.log('payOrder', data);
    await new Promise(resolve => setTimeout(resolve, 1000));

    cb();
});

Responses

ThirdPartyControllerPublicDocsExampleSocketOnSocketIOExpectedResult

Expected results

Example code, when launched, is expected to produce the following results visible in the Supa admin page:

Restaurant Account

rest-account

Staff & tips page

waiters

Payment Types

payment-types

Tables & QRs page

tables

Orders Page

orders-page

Responses

ThirdPartyPublic

ThirdPartyControllerPublicOnOrderUpsert

REQUIRED

This one should be sent as soon as the order has been created/updated or deleted. This will place a corresponding UpsertEvent in the Supa’s internal processing queue - and it will dispatch a corresponding getOrderById event as soon as possible

Authorizations:
default
Request Body schema: application/json
required
externalOrderId
required
string

Responses

Request samples

Content type
application/json
{
  • "externalOrderId": "string"
}

ThirdPartyControllerPublicOnOrderPrint

Before order bill is printed, you may call this endpoint and receive a link for a QR payment.

Authorizations:
default
Request Body schema: application/json
required
externalOrderId
string
externalTableId
string

Responses

Request samples

Content type
application/json
{
  • "externalOrderId": "string",
  • "externalTableId": "string"
}

ThirdPartyControllerPublicOnCashRegisterStart

REQUIRED

This event will trigger the synchronisation process between Supa & your POS system. Supa will shortly ask for tables, waiters, sections and other entities to get the latest updates.

Authorizations:
default

Responses

ThirdPartyControllerPublicOnPing

Every PING event will receive its “PONG” response - a healthcheck for your system to make sure the integration is up and running.

Authorizations:
default

Responses