1Overview
The RazGaz restaurant app is a client, not a server: it opens orders, reads the order board and updates orders through a public interface — and a restaurant's management system can call exactly the same interface. An integration built to this document replaces the app completely. There is no action the app can take that your system cannot.
| Authentication | The restaurant account — email and password, exchanged for a token |
| RazGaz calls | https://api.raz-gaz.com |
| Reading and updating orders | https://firestore.googleapis.com/v1/projects/razgaz-f7295/databases/(default)/documents |
| Transport | HTTPS only. Content-Type: application/json, UTF-8 |
Both hosts are one interface. Opening an order and answering a prep time are calls to RazGaz; reading the order board and acting on an existing order go straight to the database, with permission derived from that same token. A restaurant sees and updates its own orders only — the restriction is enforced server-side, not in the client.
There is no outbound channel. RazGaz does not call your system back and sends no webhook. To know what happened to an order — who picked it up, where the carrier is, whether it was delivered — you poll, §6.
2What RazGaz issues you
Three things, all from RazGaz, before the first order:
| What it is | |
|---|---|
| Email and password | The restaurant account. One account per branch — there is no single account that manages several, so a system serving two branches holds two accounts and authenticates to each separately |
| Web API key | The public project identifier needed to exchange the password for a token (§3). It is not a secret and grants nothing on its own — the password is the secret |
| Branch name | The branch's canonical name at RazGaz, exactly as it is recorded. It is the value your orders carry in restaurantName, and it is what you filter on in §6 |
No branch code and no API key like a cashier system's: on this interface the
identity is the account, and the branch is derived from it. A branch marked
inactive by RazGaz cannot open orders (403), but orders that already exist stay
readable and keep updating.
3Authentication
3.1Getting a token
POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=<WEB_API_KEY>
{
"email": "branch@example.com",
"password": "…",
"returnSecureToken": true
}
Three fields in the response are worth keeping:
| Field | What to do with it |
|---|---|
idToken | The token itself. Sent on every request, §3.3 |
refreshToken | Replaces an expired token, §3.2. Does not expire on its own |
expiresIn | Lifetime in seconds — 3600, one hour |
3.2Refreshing
POST https://securetoken.googleapis.com/v1/token?key=<WEB_API_KEY>
with grant_type=refresh_token&refresh_token=<refreshToken>, encoded as
application/x-www-form-urlencoded. The response carries a new id_token.
Do not sign in with the password on every request. Hold the token in memory
and refresh it before it expires — a minute of headroom is reasonable. An expired
token returns 401, and retrying with the same token returns the same thing
forever.
3.3Using it
On every request, to both hosts:
Authorization: Bearer <idToken>
A request with no token, an expired token, or a token belonging to another
account returns 401 or 403, and nothing is recorded — no order is opened
and nothing is updated. In production such an error means the integration is
silently delivering nothing; escalate it rather than retrying.
4Opening an order
POST /createRestaurantOrder
Opens a delivery order for the branch the account is linked to. The pickup point is neither sent nor controllable: it is the branch's stored location at RazGaz. RazGaz prices the carrier's fee, broadcasts the order to carriers in the area, and manages the carrier from assignment through delivery.
4.1Request — the required field
| Field | Type | Description |
|---|---|---|
deliveryAddress | string | The delivery address as one readable line: street, house number and town. Not used for navigation — the coordinates are — but it is what the carrier reads. Entrance details — floor, apartment, entrance — do not belong in this line; they have their own fields in §4.3. With no coordinates sent, this line is what gets looked up on the map, and a floor inside it degrades that lookup. A gate code and the like go in notes |
That is the only required field. An address is all this endpoint needs; everything else about the order — the item list and the coordinates included — is optional.
4.2Request — the drop-off point on the map
| Field | Type | Description |
|---|---|---|
deliveryLocation | object | The drop-off point on the map. { "latitude": number, "longitude": number }, both values numbers in JSON, not strings |
Recommended, not required. If your system holds coordinates, send them and we use them as they are. That is the best outcome available: a point your system confirmed beats one we inferred.
Without them we locate the address ourselves, and fill in the branch's own town when the address named none — "Herzl 5" from a Ra'anana branch is the Herzl in Ra'anana. The address stored on the order carries the town that was added (your words are kept; the town is appended), so the carrier reads a complete address.
Two things worth designing around:
- An address that cannot be located at all is a
400with a message in Hebrew. - An address that resolves only to town level gets a point at the centre of that town rather than at the building, and the order opens normally. Nothing in the response distinguishes the two — if that precision matters to you, send coordinates.
4.3Request — optional fields
| Field | Type | Description |
|---|---|---|
items | array | The order's items, { "name": string, "qty": number } each. Send them when you have them — the carrier sees the list and it is what a collection at the counter is checked against. Not required: an order sent without them, or with an empty array, is opened normally and shows as having no itemised list, which is how orders from till systems that do not send one already arrive. A value that is not an array is a 400 |
estimatedReadyMinutes | integer | Minutes from now until the food is ready. Greater than 0 and no more than 240. Send it whenever your system knows the number — see §5. On a booking (scheduledFor below) send it always: the order is released straight into dispatch and nothing asks you again, so a booking without it goes out saying the food is already up. It does not change when the order is dispatched — that is a fixed 20 minutes before the pickup time |
scheduledFor | number | string | A booking. When the carrier should arrive at the restaurant to collect the order — epoch milliseconds, or ISO-8601. The order is held and released in time for the food to be ready as the carrier reaches the counter; it is offered to no carrier before then. Must be in the future and no more than 7 days ahead, or the call is a 400 |
customerName | string | The customer's name, as the carrier needs to ask for it |
customerPhone | string | A number that can be called. The carrier calls on arrival |
floor | string | The floor. Shown to the carrier at the door, separately from the address line |
apartment | string | The apartment |
entrance | string | The entrance |
notes | string | Free text shown to the carrier. Delivery instructions, "leave at the door" |
orderAmount | number | What the customer pays for the order, in shekels |
paymentMethod | string | "cash" when the carrier must collect payment from the customer. Any other value, and the field's absence, are recorded as "card" — the carrier collects nothing |
items is stored as sent, and every screen — the carrier's, the dispatcher's —
reads exactly the two keys name and qty. An item that calls its quantity
something else shows as 1.
The three entrance fields (floor, apartment, entrance) are stored apart
from the address line and shown to the carrier as they reach the building. A
value of "0" is read as "not given" and is not shown — till systems send
"0" for a private house and the two cannot be told apart. A ground floor that
must be shown should be written in words.
4.4Reserved fields — do not send
The pickup point, the carrier's fee, the broadcast radius, the pricing region and the human-facing order number are all decided on the RazGaz side and are not taken from the request.
4.5Response
| HTTP | Body | Meaning |
|---|---|---|
201 | { "success": true, "orderId": "…" } | The order was opened |
orderId is the order's identity at RazGaz, and it is the value the rest of this
document takes: §5, §6.2 and §8 all address an order by it. Store it alongside
the order in your own system — there is no other way to point at an order, and
there is no lookup by an identifier of yours.
4.6Errors
| HTTP | When |
|---|---|
400 | deliveryAddress missing, items sent as something other than an array, or estimatedReadyMinutes out of range |
400 (Hebrew) | An address was sent with no coordinates and we could not place it on the map at all (§4.2) |
400 | The branch has no pickup location stored at RazGaz — a configuration fault; contact us |
401 | Token missing, expired or invalid (§3) |
403 | No branch is linked to the account, or the branch is marked inactive |
405 | Not a POST |
4.7A repeated call opens a second order
There is no idempotency on this call. The order id is generated on the RazGaz side on every call, and there is no field in which to send an identifier of yours, so RazGaz has no way to tell that two requests describe the same order. Retrying after a timeout where the first request did land opens a second delivery, and two carriers arrive at the same branch.
So: an automatic retry on this call is your system's responsibility, and its precondition is knowing the first one was not written. The way to check is §6.1 — poll the active orders and look for one matching the address, the phone and the time. An order opened twice by mistake is cancelled per §8.3.
5Prep time
Prep time is what lets RazGaz time the carrier to arrive at food that is ready rather than before it. Every branch has either an average prep time recorded with us, or a flag saying it gives a time per order. So a new order is in one of two situations:
estimatedReadyMinuteswas sent in §4.1, or the branch has a recorded average — the order goes out to broadcast at once. This is the intended path.- It was not sent, and the branch is flagged as giving a time per order — the
order is held at status
awaiting_prep_timeand is broadcast to no carrier until it is answered per §5.1 or §5.2. An unanswered order stays held: there is no automatic release, because broadcasting on a guess is worse than waiting. RazGaz raises an internal alert about an order nobody is answering, but that alert does not release it.
The simple way to avoid the second situation is to always send
estimatedReadyMinutes.
5.1"Ready in N minutes"
POST /submitPrepTime
{ "orderId": "…", "minutes": 20 }
minutes — a number of minutes from now, greater than 0 and no more than 240.
The call does one of two things depending on the order's state, and you do not have to choose between them:
| Order state | What happens |
|---|---|
awaiting_prep_time | Releases the order: status moves to pending and the prep clock starts now. If a RazGaz manager has put the branch on hold, it moves to held instead and a dispatcher publishes it — nothing for you to do |
| Any other active state | Updates the prep time. The status is untouched; this is the "the food will be ten minutes late" path, and a carrier already assigned is notified |
delivered / cancelled | 409 — the order is finished |
Response: { "success": true, "orderId": "…", "minutes": 20, "released": true, "readyAt": 1755513600000 }.
released says whether this was the release or the update; readyAt — epoch
milliseconds — is only present on a release.
5.2Booking an order for later
If you know the time when you open the order, send scheduledFor on
createRestaurantOrder (§4.3) and stop there — the order is booked from the
outset and this call is not needed. What follows is for an order already open:
one your system sent without a time, or one held waiting for a prep time.
POST /scheduleRestaurantOrder
{ "orderId": "…", "scheduledFor": "2026-08-18T20:30:00+03:00", "minutes": 25 }
The other answer to the same question: not how long the food takes from now but
when the carrier should come for it. RazGaz holds the order and releases it to
broadcast itself, at a time computed backwards from the requested moment through
the cooking — so the food is ready as the carrier reaches the counter. That is
why minutes is required here too: the prep time is part of the arithmetic, and
without it the release time cannot be worked out.
| Field | Type | Description |
|---|---|---|
orderId | string | The order. Must be at awaiting_prep_time |
scheduledFor | number | string | The requested pickup time — when the carrier should arrive at the restaurant: epoch milliseconds, or ISO-8601. Must be in the future and no more than 7 days ahead |
minutes | integer | The prep time, 1–240 |
Response: { "success": true, "orderId": "…", "scheduledFor": …, "releaseAt": …, "minutes": 25 }
— both stamps in milliseconds. releaseAt is the moment the order will go out to
broadcast; until then it sits at status scheduled and is offered to no carrier.
| HTTP | When |
|---|---|
400 | A time in the past, a time more than 7 days out, or minutes out of range |
404 | Unknown order |
403 | The order does not belong to this branch |
409 | The order is not at awaiting_prep_time — it has already been released, and booking it backwards is not possible from here |
An order already out to broadcast cannot be booked from this interface. If a customer moves the time after the order was released — cancel (§8.3) and open a new one at its proper time.
6Reading your orders
Everything the app displays is read here: the status, the carrier, their position
on the map, the travel times. These calls go to the second host,
firestore.googleapis.com, with the same token from §3.3. The base for every call
in this section:
https://firestore.googleapis.com/v1/projects/razgaz-f7295/databases/(default)/documents
The polling limits are part of the contract. Polling is the only way to learn what happened to an order, so its rate is not a matter of politeness:
| Permitted rate | One request every minute per branch, at most |
| When no order is active | Do not poll at all |
| Number of pollers | One per branch — not one per till or screen |
All three are spelled out because every one of them has already been broken against us in production, on another integration: one POS polls a different endpoint of ours once every 0.37 seconds — 54× the rate it was given — around the clock, including hours when the kitchen is shut, from several terminals each keeping its own timer. It did that on behalf of a single restaurant that had sent eight orders.
Nothing enforces this per request. No call of yours will be refused for exceeding it — the read path is Google's own service and there is no middleware of ours in front of it. What we see day to day is the total read volume across all branches; attributing a spike to one branch is something we turn on when a spike is what we are looking at, and it names you. Sustained excess means we contact you, and if it continues, the branch account is disabled: that is the only lever we have here, and it stops order creation too. Better to avoid that conversation — and better still not to need polling at all, which is exactly what a listener (§6.6) is for.
6.1The active orders
POST …/documents:runQuery
{
"structuredQuery": {
"from": [{ "collectionId": "orders" }],
"where": { "compositeFilter": { "op": "AND", "filters": [
{ "fieldFilter": {
"field": { "fieldPath": "restaurantName" },
"op": "EQUAL",
"value": { "stringValue": "Branch name" } } },
{ "fieldFilter": {
"field": { "fieldPath": "status" },
"op": "IN",
"value": { "arrayValue": { "values": [
{ "stringValue": "scheduled" },
{ "stringValue": "awaiting_prep_time" },
{ "stringValue": "held" },
{ "stringValue": "pending" },
{ "stringValue": "broadcasting" },
{ "stringValue": "accepted" },
{ "stringValue": "arrived_at_pickup" },
{ "stringValue": "picked_up" },
{ "stringValue": "arrived_at_delivery" }
] } } } }
] } }
}
}
Branch name is the canonical name from §2, matched character for character. A
name that is not identical returns an empty set rather than an error — the one
silent failure in this section, which is why it is worth confirming the name
against one real order before going live.
6.2A single order
GET …/documents/orders/<orderId> — with the token in the header. 404 for an
unknown order, 403 for another branch's order.
This is how to follow one order without polling the whole board, and in particular how to confirm that a call from §4 or §5 that you got no answer to did in fact land.
6.3History
The same query as §6.1 with "stringValue": "delivered" and "cancelled" in the
status list. Orders are never deleted: a delivered or cancelled order stays
readable.
6.4The fields that come back
The response is a Firestore document, meaning every value is wrapped in its type
(stringValue, integerValue, doubleValue, timestampValue, nullValue,
geoPointValue, mapValue, arrayValue). These are the fields of interest to a
restaurant system; an order carries others, all RazGaz-internal:
| Field | What it is |
|---|---|
status | The order's state, §7 |
platformOrderRef | The human-facing order number — what you say on the phone |
estimatedReadyMinutes | The prep time on record, in minutes |
readyAt | When the food is expected to be ready, per the prep time |
markedReadyAt | The moment someone said the food was up. Empty until then — §8.1 |
carrierId | The carrier's identifier, or null until one is assigned. The key for §6.5 |
estimatedArrival | RazGaz's estimate of the carrier's arrival |
scheduledFor | The pickup time on a booked order — when the carrier arrives at the restaurant, §5.2 |
cancelledAt, cancellationReason | The cancellation: when, and what was written about it |
deliveredAt | The moment of delivery to the customer |
deliveryAddress, customerName, customerPhone, items, orderAmount, paymentMethod, notes | As sent in §4 |
6.5Tracking the carrier
GET …/documents/carriers/<carrierId> — once the order carries a carrierId.
| Field | What it is |
|---|---|
displayName | The carrier's name |
phone | Their phone number — for anyone who wants a call button |
currentLocation | Their position on the map, a geoPointValue, updated while they are on shift |
vehicleType | The vehicle |
This is the information the app draws on its map. The carrier document is read whole, and anything not in this table is RazGaz-internal operations, not part of this contract — do not rely on it.
6.6Not polling at all — the recommended path
Firestore has a listen channel: you open one connection and receive every change the moment it happens. This is how the restaurant app itself works, and it is the recommended path here — it arrives instantly rather than within a minute, and it costs nothing while nothing is happening, so the limits above simply do not apply to it.
It needs the Firebase SDK, because the listen channel is gRPC and does not exist over REST. In Node:
const { initializeApp } = require('firebase/app');
const { getAuth, signInWithEmailAndPassword } = require('firebase/auth');
const { getFirestore, collection, query, where, onSnapshot } = require('firebase/firestore');
const app = initializeApp({ apiKey: '<WEB_API_KEY>', projectId: 'razgaz-f7295' });
await signInWithEmailAndPassword(getAuth(app), 'branch@example.com', '…');
onSnapshot(
query(
collection(getFirestore(app), 'orders'),
where('restaurantName', '==', 'Branch name'),
where('status', 'in', ['scheduled', 'awaiting_prep_time', 'held', 'pending', 'broadcasting',
'accepted', 'arrived_at_pickup', 'picked_up', 'arrived_at_delivery']),
),
(snap) => {
for (const change of snap.docChanges()) {
// 'added' — an order joined the board · 'modified' — status, carrier or ETA changed
console.log(change.type, change.doc.id, change.doc.data().status);
}
},
);
The query is identical to §6.1's, and so are the permissions.
signInWithEmailAndPassword holds and refreshes the token itself, so §3.2 is not
needed on this path. The fields are the same ones as §6.4, except they arrive as
plain values rather than wrapped in Firestore types.
What arrives is every change to the document, not only a status change — the carrier's ETA updates too, which are written every two minutes while an order is on its way. That is what lets you show an ETA that moves, and it is also what sets the cost: a few dozen changes per order, and nothing between orders.
When to poll anyway. When there is no SDK — a PHP or older .NET stack that cannot hold a connection — §6.1's polling is the path, subject to the limits at the top of §6.
7Statuses
status | Meaning |
|---|---|
awaiting_prep_time | Held: RazGaz is waiting for a prep time. Broadcast to no carrier — §5 |
held | Held by a RazGaz dispatcher: the branch is set to manual publishing, and a dispatcher releases each order. Broadcast to no carrier; nothing for the branch to do — cook as usual |
scheduled | Booked for later, awaiting automatic release — §5.2 |
pending | Released, broadcast not yet started |
broadcasting | Being offered to carriers in the area |
accepted | A carrier took it and is on the way to the branch |
arrived_at_pickup | The carrier is at the branch |
picked_up | The food is with the carrier, on the way to the customer |
arrived_at_delivery | The carrier is at the customer |
delivered | Delivered. Final |
cancelled | Cancelled. Final |
The status advances in that order, but not every state necessarily appears in your polling: two adjacent states can both pass between two samples. Treat the status as a current state rather than an event, and do not build logic that requires seeing every one.
8Acting on an existing order
The three actions the app offers on an order card. All of them are
POST …/documents:commit against the host from §6, with the same token, and in
exactly this shape: the shapes below are what is permitted, and any other write
to an order is rejected server-side — a field not on the list, or another
branch's order, comes back as a permission error rather than a partial write.
In every request <orderId> is the identifier from §4.5, and name is the full
path projects/razgaz-f7295/databases/(default)/documents/orders/<orderId>.
8.1The food is ready
{ "writes": [ {
"update": {
"name": "projects/razgaz-f7295/databases/(default)/documents/orders/<orderId>",
"fields": { "readySource": { "nullValue": null } }
},
"updateMask": { "fieldPaths": ["markedReadyAt", "readySource"] },
"updateTransforms": [
{ "fieldPath": "markedReadyAt", "setToServerValue": "REQUEST_TIME" }
]
} ] }
This is the message to the carrier that there is nothing left to wait for.
markedReadyAt is written as server time — not your system's clock — because
everything on the other side reads it as RazGaz time, and a clock a few minutes
off moves the message. readySource: null is sent in the same write; the field
records who said the food was up, and since 19 August 2026 the answer is always
a person.
Nothing else ever writes it. A prep time is an estimate, and letting one expire into "ready" told a carrier the food was up on nobody's word. So an estimate that runs out changes nothing about the order: if this call is never made, the carrier is simply never told, for the whole life of the order. It is still dispatched, claimed and delivered normally. Send it when the bag is on the counter and not before.
Call it once. A second call writes a fresh stamp over the first and adds nothing.
8.2Updating the prep time
Not here — §5.1. submitPrepTime is correct in both situations, writes the same
field, and also notifies a carrier already assigned. Writing
estimatedReadyMinutes directly is possible and notifies nobody; there is no
reason to prefer it.
8.3Cancelling
{ "writes": [ {
"update": {
"name": "projects/razgaz-f7295/databases/(default)/documents/orders/<orderId>",
"fields": {
"status": { "stringValue": "cancelled" },
"cancellationReason": { "stringValue": "Cancelled by the restaurant" }
}
},
"updateMask": { "fieldPaths": ["status", "cancelledAt", "cancellationReason"] },
"updateTransforms": [
{ "fieldPath": "cancelledAt", "setToServerValue": "REQUEST_TIME" }
]
} ] }
"cancelled" is the only value for status supported from this interface. The
other statuses describe what is happening to the carrier and RazGaz is what
writes them; an order marked from outside into a state that does not match what
is actually happening comes apart from what the dispatcher and the carrier see.
cancellationReason is read by a person, so make it concrete — "customer
cancelled", "branch closed" — rather than empty.
A cancellation is final and does not undo itself. A cancelled order does not
return to broadcast and cannot be revived; what follows it is a new order per §4.
Cancelling an order the carrier has already collected does not bring the food
back — at picked_up and beyond, call the dispatch desk rather than relying on
this call alone.
9What this interface does not have
Worth knowing before you design, so that nothing is designed around it:
| A callback from RazGaz | None. No webhook and no push to your system; information arrives by polling, §6 |
| Address-to-coordinate lookup | Since 2026-08-25 there is one: send the address without coordinates and we locate it, filling in the branch's own town when the address named none (§4.2). Sending coordinates is still the more precise input, and the only one that guarantees a building rather than a town centre |
| An order identifier of your own | None. The identifier is RazGaz's, and there is no lookup by yours — §4.5, §4.7 |
| Changing the address or customer details after opening | None. An order opened with a wrong address is cancelled and reopened |
| Choosing a carrier, or changing region and pricing | None. Carrier assignment, their fee and the broadcast radius are RazGaz's |
| Booking an order that has already been released | None. Booking is only possible from awaiting_prep_time — §5.2 |
| One account for several branches | None. One account per branch, §2 |
10Before going live
In this order, against one real branch:
- Authentication returns a token, and refresh works. Not just the sign-in — the path after an hour too, because in production that is the one you meet constantly.
- A test order opens and returns
201and anorderId, and theorderIdis stored on your side alongside the order. - The §6.1 poll returns exactly that order. If it returns an empty set, the branch name is not identical to the canonical one. This is the common fault.
- The status moves. From
pendingtobroadcastingand on toacceptedwhen a carrier takes the order, at which pointcarrierIdfills in and §6.5 returns their position. - "The food is ready" (§8.1) writes —
markedReadyAtfills in on the next poll. - Cancelling (§8.3) works on the order you opened for the test, and closes it. Finish on that, so a test delivery is not left open in broadcast.
estimatedReadyMinutesis sent on every order — or it has been confirmed with RazGaz that the branch has an average prep time on record. Otherwise the first order in production will sit held, §5.
For questions and integration faults, the address is whoever gave you this
document. An error that repeats in production — particularly a 401, a 403, or
a poll that comes back empty — is an integration silently delivering nothing;
escalate it immediately rather than waiting for the next order.