Sales API

The Sales API lets an automation tool send your POS sales to Rinvy as a web request, so nobody has to upload a file. Zapier, Make, IFTTT and a Google Sheets script all work. You create a kitchen API key in Kitchen Settings, then send each day's item totals to one Rinvy address.

Sales sent this way land in the same place as a CSV upload. Lines that match a menu item are recorded right away. Lines that don't match wait for you in Pending Review on the Sales Data page.

Create an API key

  1. Go to Kitchen Settings and open the Integrations section.

  2. Under API access, type a name for the key. Name it after the tool that will use it, for example "Zapier".

  3. Press Create key.
  4. Press Copy key and paste it into your tool. Then press Done.

The Copy your API key sheet with a one-time warning, the new key, and the Done and Copy key buttons

A kitchen can have up to 5 active keys. Only owners can see, create or revoke them. The list shows when each key was created and when it was last used.

The API access section with the Key name field, the Create key button, and two keys in the list, each with its creation date, last use and a Revoke button

To stop a key, press Revoke next to it. Anything that uses the key stops working right away. Revoked keys stay in the list so you can see what they were.

Send sales

Send a POST request to:

https://api.rinvy.app/api/v1/sales

Put the key in the Authorization header as Bearer <your key>, and send the body as JSON with Content-Type: application/json. Rinvy refuses a key in the address (?api_key=), because addresses end up in logs. The bearer token standard, RFC 6750, advises against tokens in the address for the same reason.

Where to put the header in common tools:

  • Zapier (Webhooks by Zapier, POST): add Authorization with the value Bearer <your key> under Headers.
  • Make (HTTP, Make a request): add a header named Authorization with the value Bearer <your key>.
  • IFTTT (Webhooks, Make a web request, which needs IFTTT Pro): in Additional Headers, type Authorization: Bearer <your key>. Set Content Type to application/json.
  • Google Apps Script: see the example below.

The request body

{
  "salesDateStart": "2026-09-26",
  "salesDateEnd": "2026-09-26",
  "items": [
    { "posItemName": "Margherita Pizza", "quantitySold": 12, "netSales": 168.0 },
    { "posItemName": "Caesar Salad", "quantitySold": 7, "netSales": 77.0 }
  ]
}
  • salesDateStart and salesDateEnd are the business days the sales cover, as YYYY-MM-DD. They can't be in the future in your kitchen's timezone.
  • Each item needs posItemName (the name your POS uses) and quantitySold (more than 0). netSales and grossSales are optional.
  • You can send up to 2,000 items in one request.
  • To send more than one day in one request, give every item a dailyBreakdown that splits it by day. Every day in it must be inside the date range, and the days must add up to the item's quantitySold and netSales:
{
  "posItemName": "Margherita Pizza",
  "quantitySold": 20,
  "netSales": 280.0,
  "dailyBreakdown": [
    { "businessDate": "2026-09-25", "qty": 8, "netSales": 112.0 },
    { "businessDate": "2026-09-26", "qty": 12, "netSales": 168.0 }
  ]
}

Sending the same days again

Each request is the whole story for its days. If Rinvy already has sales for any of those days, the request is refused with a 409 and nothing changes. That makes a retry safe.

To replace days you already sent, for example after a correction in the POS, add "replace": true to the body. Rinvy then swaps the old sales for those days with the new ones.

The response

A successful request returns:

{
  "ok": true,
  "uploadId": "…",
  "status": "COMPLETED",
  "matchedCount": 2,
  "unmatchedCount": 0
}

status is COMPLETED when every line matched a menu item, or PENDING_REVIEW when some lines wait for you on the Sales Data page. The upload shows there as via API key: Zapier (the name of your key).

A refused request returns "ok": false and an error that says what to fix:

StatusMeaning
400The body is wrong, for example a missing field or a future date.
401The key is missing, wrong, revoked, or sent in the address instead of the header.
403Your plan doesn't include sales data.
409Rinvy already has sales for these days. Send "replace": true to replace them.
429Too many requests. A key can send 60 requests an hour. Wait, then try again.

How do I test my key before I connect Zapier?

Run this curl command in a terminal, with your key in place of rk_live_YOUR_KEY. A 401 means the key is wrong or revoked. A 200 with "ok": true means the key works and Rinvy accepted the test sale, so send that day's real numbers with "replace": true afterwards.

curl -X POST https://api.rinvy.app/api/v1/sales \
  -H "Authorization: Bearer rk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "salesDateStart": "2026-09-26",
    "salesDateEnd": "2026-09-26",
    "items": [{ "posItemName": "Margherita Pizza", "quantitySold": 12, "netSales": 168.0 }]
  }'

Google Sheets example

The Google Apps Script below reads yesterday's rows from a sheet and sends them as one day. It expects a tab named Sales with the columns Date, Item, Qty and Net sales (the date as a real date, not text).

  1. In your Google Sheet, open Extensions → Apps Script.
  2. Paste the script below and put your key in RINVY_API_KEY.
  3. Run sendYesterday once and approve the permissions it asks for.
  4. Open Triggers (the clock icon), add a trigger for sendYesterday, and pick a daily time-driven schedule, for example between 5 and 6 AM.

const RINVY_API_KEY = 'rk_live_YOUR_KEY';
const RINVY_URL = 'https://api.rinvy.app/api/v1/sales';

function sendYesterday() {
  const tz = Session.getScriptTimeZone();
  const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
  const day = Utilities.formatDate(yesterday, tz, 'yyyy-MM-dd');

  const rows = SpreadsheetApp.getActive().getSheetByName('Sales').getDataRange().getValues();
  const totals = {};
  rows.slice(1).forEach(([date, item, qty, net]) => {
    if (!item || Utilities.formatDate(new Date(date), tz, 'yyyy-MM-dd') !== day) return;
    totals[item] = totals[item] || { posItemName: String(item), quantitySold: 0, netSales: 0 };
    totals[item].quantitySold += Number(qty) || 0;
    totals[item].netSales += Number(net) || 0;
  });

  const items = Object.values(totals).filter((i) => i.quantitySold > 0);
  if (items.length === 0) return;

  const response = UrlFetchApp.fetch(RINVY_URL, {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + RINVY_API_KEY },
    payload: JSON.stringify({ salesDateStart: day, salesDateEnd: day, replace: true, items }),
    muteHttpExceptions: true,
  });
  Logger.log(response.getResponseCode() + ' ' + response.getContentText());
}

The script sends replace: true, so running it twice for the same day is safe: the second run replaces the first.

Google runs a daily time-driven trigger at a time it picks inside the hour you choose, then keeps that time from day to day.

Related