Overview
Base URL: https://api.imxi.mt/v1/partner. JSON in, JSON out, UTF-8.
- Times are ISO 8601 with an offset, like
2026-10-09T19:00:00+02:00. - Days are
YYYY-MM-DDin Malta time (Europe/Malta). - Money is in cents.
Authentication
The venue's owner or an admin creates a key in the Imxi app: Your business, Tables, Connect other systems, Create a key. A key looks like imxi_live_ followed by 43 characters. It is shown once: a lost key is replaced, not recovered. A venue can have up to 10 live keys. Revoking a key stops it at once.
Send it on every request:
Authorization: Bearer imxi_live_Jq3...curl https://api.imxi.mt/v1/partner/venue \
-H "Authorization: Bearer $IMXI_KEY"A key sees and changes only its own venue. Another venue's booking answers 404 booking_not_found. Every write is logged with the key that made it.
Scopes
Scopes are chosen when the key is created.
| Scope | Allows |
|---|---|
availability:read | GET /availability, GET /availability/days, GET /tables |
bookings:read | GET /bookings, GET /bookings/:ref, GET /waitlist. Phone numbers and emails come back as null. |
bookings:write | POST /bookings, PATCH /bookings/:ref, POST /bookings/:ref/cancel, POST /bookings/:ref/status |
guests:read | With bookings:read: the phone number and email on bookings and the waitlist. |
GET /venue works with any key.
Errors
Every error has the same shape:
{ "error": { "code": "table_taken", "message": "That time has just gone. Pick another." } }message is written for a person and safe to show. Branch on code.
| Status | Codes |
|---|---|
| 400 | validation, not_a_time, time_passed, lead_time, too_far, party_too_big, phone_invalid, range_too_long, range_invalid, date_invalid, status_invalid, tables_not_enabled |
| 401 | invalid_api_key (unknown, malformed or revoked) |
| 403 | scope_missing |
| 404 | booking_not_found, not_found |
| 409 | table_taken, venue_closed, online_limit_reached, booking_not_active, booking_transition |
| 422 | policy_violation (free text that breaks Imxi's content policy) |
| 429 | rate_limited |
| 502 | payment_setup_failed, refund_failed |
Rate limits
120 requests a minute per key. Past that, 429 rate_limited: wait and retry. Requests with a key that is not valid are limited per address: 20 in ten minutes, then 429 until the ten minutes pass.
A free venue takes 50 online bookings a month (app, web, Google and API together). Past that POST /bookings answers 409 online_limit_reached until the month ends. Venue Pro has no limit.
Endpoints
All paths are under https://api.imxi.mt/v1/partner. In the paths below, :ref is a booking's id or its six character code.
GET/venueany key
The venue the key belongs to, and the key itself.
{
"id": "5a0c…", "slug": "ta-kris", "name": "Ta' Kris",
"address": "80 Fawwara Lane", "locality": "Sliema",
"phone": "+35621337367", "timezone": "Europe/Malta",
"key": { "id": "…", "name": "Website", "scopes": ["availability:read", "bookings:read", "bookings:write"] }
}GET/availabilityavailability:read
The bookable times of a day for a party, as a guest sees them. Query: date, partySize, and an optional area (inside, terrace, bar or the venue's own).
curl "https://api.imxi.mt/v1/partner/availability?date=2026-10-09&partySize=4" \
-H "Authorization: Bearer $IMXI_KEY"{
"date": "2026-10-09", "partySize": 4, "open": true, "closedReason": null,
"requestOnly": false, "limitReached": false, "durationMinutes": 90,
"areas": ["inside", "terrace"], "policyNote": "We hold tables for 15 minutes.",
"periods": [
{ "key": "dinner", "name": "Dinner", "times": [
{ "time": "19:00", "startsAt": "2026-10-09T17:00:00.000Z", "available": true, "deposit": null },
{ "time": "19:30", "startsAt": "2026-10-09T17:30:00.000Z", "available": false, "reason": "full", "deposit": null }
] }
]
}reason is full, pacing, notice, blocked or past. deposit is {kind, amountCents, perPersonCents, currency, cancelHours} when the venue's deposit applies to that time. requestOnly is true for a party over the venue's online limit: the times are sent to the venue as a request.
GET/availability/daysavailability:read
A summary per day. Query: from, days (up to 31) and partySize, for example ?from=2026-10-09&days=14&partySize=2.
{ "items": [{ "date": "2026-10-09", "open": true, "available": 9, "firstTime": "12:00" }] }GET/tablesavailability:read
{ "items": [{ "id": "…", "name": "4", "minSeats": 2, "maxSeats": 4, "area": "inside", "joinableWith": ["…"], "isActive": true, "sort": 3 }] }The booking object
{
"id": "0d3b1a52-…", "code": "K7M2QX", "status": "confirmed",
"startsAt": "2026-10-09T17:00:00.000Z", "endsAt": "2026-10-09T18:30:00.000Z", "durationMinutes": 90,
"partySize": 4, "area": "inside", "tables": [{ "id": "…", "name": "4", "area": "inside" }],
"occasion": "Birthday", "note": "Window if you can", "venueNote": null,
"source": "web", "externalRef": null,
"guest": { "name": "Maria Borg", "phone": "+35679000111", "email": "maria@example.com" },
"deposit": {
"status": "paid", "state": "paid", "amountCents": 4000, "currency": "eur",
"refundableUntil": "2026-10-08T17:00:00.000Z", "paidAt": "2026-10-02T09:12:44.000Z", "refundedAt": null
},
"arrivedAt": null, "seatedAt": null, "cancelledAt": null,
"createdAt": "2026-10-02T09:12:01.000Z", "updatedAt": "2026-10-02T09:12:44.000Z"
}status:requested(waiting for the venue to confirm),confirmed,declined,cancelled,no_show,completed.source:app,web,google,phone,walk_in,api.deposit.state:pending,paid,redeemed(taken off the bill),kept,refunded,failed.depositisnullwhen there is none.guest.phoneandguest.emailarenullunless the key hasguests:read.externalRefis your own reference, when the booking was made through the API with one.
GET/bookingsbookings:read
Table bookings that start in the range, oldest first. from and to are days (both included) or instants. The default is the next 7 days, and a range covers at most 31 days. Optional status, limit (1 to 100, default 20) and cursor.
curl "https://api.imxi.mt/v1/partner/bookings?from=2026-10-09&to=2026-10-11" \
-H "Authorization: Bearer $IMXI_KEY"Returns { "items": [booking], "nextCursor": "…" | null }.
GET/bookings/:refbookings:read
One booking.
POST/bookingsbookings:write
curl -X POST https://api.imxi.mt/v1/partner/bookings \
-H "Authorization: Bearer $IMXI_KEY" \
-H "Content-Type: application/json" \
-d '{
"startsAt": "2026-10-09T19:00:00+02:00",
"partySize": 4,
"guest": { "name": "Maria Borg", "phone": "+35679000111", "email": "maria@example.com" },
"area": "inside",
"occasion": "Birthday",
"note": "Window if you can",
"externalRef": "order-10432",
"deposit": "skip"
}'Only startsAt, partySize and guest.name are required. The booking is made with source api under the rules a guest books under: startsAt must be one of the venue's seating times, with the venue's notice, how far ahead it opens, pacing and the online party limit. An eight digit phone number is read as Maltese. Anything else needs its country code.
externalRef is your own reference. Sending the same one again returns the booking it made the first time with 200 instead of 201, so a request that timed out is safe to repeat.
deposit: "skip" (the default) books without the venue's deposit. "collect" holds the table for 10 minutes and returns a Viva.com checkout link for the guest to pay. The booking is requested until it is paid and is released if it is not.
Returns 201:
{
"booking": { "id": "0d3b1a52-…", "code": "K7M2QX", "…": "the booking object" },
"checkout": null
}With "deposit": "collect" and a deposit due, checkout is set:
"checkout": { "checkoutUrl": "https://…", "expiresAt": "…", "amountCents": 4000, "currency": "eur" }Imxi does not email guests of bookings made through the API. Your system tells them.
PATCH/bookings/:refbookings:write
Any of startsAt, partySize, tableIds and venueNote. Moves follow the rules the venue's staff have in the diary: any time with a free table, and tableIds to choose the tables. A guest with the Imxi app is told when the time moves. Returns the booking.
curl -X PATCH https://api.imxi.mt/v1/partner/bookings/K7M2QX \
-H "Authorization: Bearer $IMXI_KEY" \
-H "Content-Type: application/json" \
-d '{ "startsAt": "2026-10-09T20:00:00+02:00", "partySize": 5, "venueNote": "High chair" }'POST/bookings/:ref/cancelbookings:write
reason is optional. This is the venue cancelling: a paid deposit goes back to the guest in full, and a guest with the app is told, with the reason. Cancelling a booking that is already cancelled returns it unchanged. Returns the booking.
curl -X POST https://api.imxi.mt/v1/partner/bookings/K7M2QX/cancel \
-H "Authorization: Bearer $IMXI_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Kitchen closed" }'POST/bookings/:ref/statusbookings:write
status is arrived, seated, completed, no_show or confirmed. A no-show keeps a paid deposit. confirmed accepts a requested booking. Returns the booking.
curl -X POST https://api.imxi.mt/v1/partner/bookings/K7M2QX/status \
-H "Authorization: Bearer $IMXI_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "seated" }'GET/waitlistbookings:read
The waitlist for a day: ?date=2026-10-09. status is waiting, offered (a table is held for the party until offer.expiresAt) or booked. The guest's phone and email need guests:read.
{
"items": [{
"id": "…", "day": "2026-10-09", "partySize": 2,
"earliestTime": "19:00", "latestTime": "21:00",
"status": "waiting", "offer": null, "bookingId": null,
"source": "web", "createdAt": "…",
"guest": { "name": "Walt", "phone": null, "email": null, "userId": null }
}]
}Webhooks
The owner or an admin adds a webhook in the app: Tables, Connect other systems, Add a webhook. A venue can have up to 5. Each has an https address, the events it wants (none chosen means all) and a signing secret, whsec_…, shown once and stored encrypted.
Events:
booking.created,booking.updated,booking.cancelledbooking.seated,booking.completed,booking.no_showdeposit.paid,deposit.refundedwaitlist.joinedping, from “Send test event”
A booking with a deposit sends booking.created and deposit.paid together, once it is paid. A table that is only held while the deposit is being paid sends nothing. A cancellation that refunds sends booking.cancelled and deposit.refunded.
Each event is one POST with a JSON body:
{
"id": "b0a8c3de-…",
"event": "booking.created",
"createdAt": "2026-10-02T09:12:44.512Z",
"venue": { "id": "5a0c…", "slug": "ta-kris", "name": "Ta' Kris" },
"data": {
"booking": { "id": "0d3b1a52-…", "code": "K7M2QX", "status": "confirmed", "…": "the booking object, with the guest's phone and email" }
}
}waitlist.joined carries data.waitlist. ping carries data.message. Webhooks always carry the guest's phone and email.
Headers: Content-Type: application/json, User-Agent: Imxi-Webhooks/1.0, Imxi-Event, Imxi-Delivery (the same value as id) and the signature:
Imxi-Signature: t=1791105164,v1=5f8c…Verifying signatures
v1 is the hex HMAC-SHA256, keyed with the webhook's secret, of the string <t>.<raw request body>. Verify it before trusting the body, compare in constant time, and refuse a t more than five minutes old:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyImxi(rawBody, header, secret) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
const expected = createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest();
return timingSafeEqual(expected, Buffer.from(m[2], "hex"));
}Use the raw bytes of the body, before any JSON parsing.
Retries
Answer with any 2xx within 8 seconds. Anything else is a failure, redirects included: Imxi never follows a redirect.
A failed delivery is tried again after 1, 5 and 15 minutes, then 1, 3, 6 and 12 hours, eight attempts in all, each with the same id, so ignore an id you have already handled. Events can arrive out of order. data.booking.updatedAt says which is newer.
After 10 failed attempts in a row the webhook is switched off, what was still queued is dropped, and the venue's owner and admins get a notification. Switching it back on in the app starts the count again. “Send test event” sends one ping straight away and shows the result. A failed test does not count.
Where a webhook may point
httpsonly, with no username or password in the address.- Every address the name resolves to must be on the public internet.
- Loopback, private (10/8, 172.16/12, 192.168/16), link-local (169.254/16, including cloud metadata), carrier NAT, multicast and reserved addresses are refused, and so are their IPv6 counterparts. That holds when they hide behind a public hostname, an IPv4-mapped IPv6 address or NAT64.
- The check runs when the webhook is saved and again before every attempt, and the connection is made to the address that was checked.
Calendar feed
In the app: Tables, Connect other systems, Calendar link. A read-only iCal address, https://api.imxi.mt/v1/public/calendar/<secret>.ics, to subscribe to in Google Calendar, Outlook or Apple Calendar.
- It lists the venue's table bookings from a week back to two months ahead: the name, the party size, the tables, notes and the reference.
- Phone numbers and emails are left out.
- The address is the secret: anyone who has it can read the feed.
- “Replace link” makes a new address and the old one stops working. “Switch off” removes it.
- Calendar apps fetch it on their own schedule. Google can take several hours.
Booking button for your website
No key needed. Every venue with table bookings has a public booking page at https://imxi.mt/book/your-venue, and one script tag puts it on your own site. The app has these snippets with your venue filled in.
A button that opens the booking form in a dialog:
<script async src="https://imxi.mt/embed.js?v=1" data-venue="your-venue"></script>The form in the page itself, resized to fit:
<script async src="https://imxi.mt/embed.js?v=1" data-venue="your-venue" data-mode="inline"></script>For site builders that strip scripts, a plain frame with a fixed height:
<iframe src="https://imxi.mt/book/your-venue?embed=1" title="Book a table" loading="lazy" style="width:100%;max-width:560px;height:760px;border:0"></iframe>| Attribute | What it does |
|---|---|
data-venue | The last part of your booking page's address. Required. |
data-mode | button (the default) or inline. |
data-label | The button's text, up to 40 characters. The default is “Book a table”. |
data-colour | The button's colour as #rgb or #rrggbb. The text turns black or white to suit. data-color works too. |
data-target | A CSS selector to draw into. Without it the button or form appears straight after the script tag. |
data-source | google counts the bookings as coming from your Google listing. |
To use a button of your own, give any element data-imxi-book="your-venue" and keep the script tag on the page. A click on it opens the same dialog.
The script sets no cookies, stores nothing and makes no requests of its own. A deposit is always paid in the full browser window, never inside the frame. The form tells your page what happens with postMessage: { type: "imxi:booked", code } after a confirmed booking and { type: "imxi:height", height } when its height changes. Check that event.origin is https://imxi.mt before you trust a message.
Not here yet
Guest book endpoints, changing tables and hours, creating webhooks through the API, and the Reserve with Google partner integration. The booking link on a Google listing works today.
Questions? Email hello@imxi.mt. Prices and what Venue Pro adds are on the pricing page.