Sales API

If your POS can export a sales report, and you have a tool that can send a web request, you can push sales into Rinvy without uploading a file. Zapier, Make, IFTTT and a Google Sheets script all work.

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.

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.

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.

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.

Try it with curl

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

This script 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.

Related