# iJava Developer Docs — MCP Server and REST API

Version 1.2.0 · Updated 2026-10-08 · Endpoint: https://ijavacafe.com/api/mcp
Web version: https://ijavacafe.com/docs

## Overview

Two ways to order real food from iJava Cafe in San Jose from software. The **MCP server** lets an AI assistant read the cafes and the live menu, build a pickup order, hand the customer a secure payment link, and check the order afterwards. The [REST API](https://ijavacafe.com/docs#rest-overview) lets your own app place orders from a prepaid balance that go straight to the kitchen.

It speaks the [Model Context Protocol](https://modelcontextprotocol.io), so Claude, Cursor and any other MCP client can use it with no SDK and no glue code.

|  |  |
| --- | --- |
| Endpoint | `https://ijavacafe.com/api/mcp` |
| Transport | Streamable HTTP, stateless. One JSON-RPC 2.0 message per `POST`, answered with JSON. |
| Protocol versions | `2025-06-18`, `2025-03-26`, `2024-11-05` |
| Authentication | None. See [Authentication and limits](https://ijavacafe.com/docs#auth). |
| Fulfilment | Pickup only. AI orders are open at Willow Glen. |
| Payment | The customer pays on a Stripe page. The server never charges a card by itself. |

### Open by design

The MCP server has no API keys, no customer accounts and no saved cards. This is on purpose, so any assistant can connect in a minute. An app that should order automatically on its own card uses the [REST API](https://ijavacafe.com/docs#rest-overview) instead, which has keys and billing.

- **Your agent fills in the customer.** It passes the customer's name, email and phone to `create_order` when it places the order.
- **The customer confirms by paying.** They open the payment link, check the items and the total, and pay on Stripe's page.
- **Nothing is charged without the customer.** The server never stores a card and never charges one by itself. An unpaid order never reaches the kitchen.

### What is available

| Piece | Status |
| --- | --- |
| MCP tools: `list_locations`, `search_menu`, `create_order`, `get_order_status` | **Live** |
| REST API `/api/v1` with keys, a sandbox and prepaid credits — see [REST API](https://ijavacafe.com/docs#rest-overview) | **Live** |
| Webhooks: `order.paid`, `order.printed`, `order.refunded`, `order.canceled` — see [Webhooks](https://ijavacafe.com/docs#rest-webhooks) | **Live** |

## Quickstart

Add the server to your client, then ask it something like *"What cold drinks does iJava Willow Glen have?"*

**Claude Code**

```bash
claude mcp add --transport http ijava https://ijavacafe.com/api/mcp
```

**Claude.ai / Desktop**

```text
Settings → Connectors → Add custom connector
Name: iJava
URL:  https://ijavacafe.com/api/mcp
```

**Cursor**

```json
{
  "mcpServers": {
    "ijava": {
      "url": "https://ijavacafe.com/api/mcp"
    }
  }
}
```

Add to `~/.cursor/mcp.json`, or `.cursor/mcp.json` in your project.

**Other clients**

```json
{
  "mcpServers": {
    "ijava": {
      "type": "http",
      "url": "https://ijavacafe.com/api/mcp"
    }
  }
}
```

Most clients take an `mcpServers` entry like this one. Pick the Streamable HTTP transport, not SSE.

**curl**

```bash
curl -s https://ijavacafe.com/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_menu","arguments":{"location":"almaden","query":"latte"}}}'
```

No key, no sign-up and no customer account. Every tool works the moment the client connects.

## How ordering works

1. **Pick a cafe.** Call `list_locations`. Use a cafe where `online_pickup_open` is `true`.
2. **Find the items.** Call `search_menu` to get each `item_id`, its sizes, and its required options.
3. **Confirm with the customer.** Read back the items and the price before you order.
4. **Create the order.** Call `create_order`. You get an `order_number` and a `payment_url`.
5. **Hand over the link.** The server also emails and texts it to the customer by default. They pay on Stripe's page, and the order goes to the kitchen the moment they pay.
6. **Check on it.** Call `get_order_status` with the order number and the customer's email.

> **Note:** Every order works this way. The agent prepares the order, and the customer approves it by paying. There are no saved cards and no automatic charges, so the customer always sees the total before they pay.

When a cafe is not taking online pickup orders, `create_order` refuses with a link where the customer can order instead. Pass that link on.

## Authentication and limits

The server needs no key, and that will not change. The tools only read the public menu, or create an order the customer still has to pay for. Customer details are never returned, except to someone who already has both the order number and the email.

Your agent supplies the customer's name, email and phone on each `create_order` call. There is no customer account to sign in to, and no token to store. Use the details your user gave you, and confirm them before you order. The receipt goes to that email, and `get_order_status` needs it.

### Rate limits

| Limit | Per |
| --- | --- |
| 120 requests a minute | caller |
| 5 `create_order` calls a minute | caller |
| 20 `get_order_status` calls a minute | caller |
| 30 `create_order` calls a minute | all callers together |

Past a limit the server answers HTTP `429` with JSON-RPC error `-32000`. Wait a minute and try again. Request bodies over 64 KB get HTTP `413`.

## Tool: `list_locations`

_List iJava cafes. Read-only._

iJava cafe locations with address, phone, weekly hours, and whether online pickup orders are open right now.

### Arguments

None. Send an empty object, `{}`.

### Example

**Request**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_locations",
    "arguments": {}
  }
}
```

**Result (the text of content[0], parsed)**

```json
[
  {
    "location": "delmas",
    "name": "iJava Cafe — Delmas",
    "address": "387 Delmas Ave, San Jose, CA 95126",
    "phone": "(408) 753-9732",
    "hours": {
      "sun": "06:30-14:00",
      "mon": "06:30-14:00",
      "…": "…"
    },
    "online_pickup_open": false,
    "order_elsewhere": "https://order.toasttab.com/online/ijava-cafe-delmas-387-delmas-avenue"
  },
  {
    "location": "almaden",
    "name": "iJava Cafe — Willow Glen",
    "address": "2306 Almaden Rd #150, San Jose, CA 95125",
    "phone": "(408) 979-0251",
    "hours": {
      "sun": "08:00-14:00",
      "mon": "08:00-14:00",
      "…": "…"
    },
    "online_pickup_open": true,
    "next_pickup": "ASAP"
  }
]
```

Hours are Pacific time, 24-hour clock. When `online_pickup_open` is `true` the result has `next_pickup` (`ASAP`, or the next opening such as `Saturday at 8:00 AM`). When it is `false` the result has `order_elsewhere`, a link the customer can order from instead.

## Tool: `search_menu`

_Search the menu. Read-only._

Search one cafe's orderable menu. Returns item_id, price, sizes and modifiers, which create_order needs. Prices are in USD before tax.

### Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `location` | `"delmas" \| "almaden"` | yes | delmas = Downtown San Jose, almaden = Willow Glen |
| `query` | `string` | no | Words to match in the item name, description or section. Omit to list the whole menu. (max 80 chars) |

### Example

**Request**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_menu",
    "arguments": {
      "location": "almaden",
      "query": "cold brew"
    }
  }
}
```

**Result (the text of content[0], parsed)**

```json
{
  "location": "almaden",
  "count": 1,
  "items": [
    {
      "item_id": "cold-brew",
      "name": "Cold Brew",
      "section": "Coffee",
      "price": "$7.00",
      "modifiers": [
        {
          "name": "Milk",
          "choose": "exactly 1 (required)",
          "options": [
            "Regular milk",
            "Oat milk (+$0.75)",
            "Black (no milk)"
          ]
        },
        {
          "name": "Extras",
          "choose": "any (optional)",
          "options": [
            "Vanilla (+$0.75)",
            "Extra shot (+$0.75)"
          ]
        }
      ]
    }
  ]
}
```

Every word in `query` must match the item name, description, section or category. Omit `query` to get the whole orderable menu for that cafe (about 120 items).

Sized items return `sizes`, and you must send one of them as `size` in `create_order`. A modifier marked **required** must be sent. Send option names without the `(+$0.75)` suffix. Items the cafe has run out of today carry `"sold_out": true` and will be refused.

## Tool: `create_order`

_Create a pickup order. Creates an order._

Prices a pickup order and returns a secure Stripe payment link. By default the link is also emailed and texted to the customer. Nothing is charged until the customer opens the link and pays. The kitchen gets the order once payment goes through. Confirm the items and total with the customer before calling this.

### Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `location` | `"delmas" \| "almaden"` | yes | delmas = Downtown San Jose, almaden = Willow Glen |
| `customer` | `object` | yes | The person picking up |
| `customer.name` | `string` | yes | Name on the pickup ticket (max 120 chars) |
| `customer.email` | `string` | yes | Gets the receipt; needed for get_order_status (email, max 320 chars) |
| `customer.phone` | `string` | yes | The cafe calls this number if there is a problem (max 40 chars) |
| `lines` | `object[]` | yes | One entry per menu item. At most 40 items in all (1–40 items) |
| `lines[].item_id` | `string` | yes | From search_menu |
| `lines[].quantity` | `integer` | yes | How many of this item (1–20) |
| `lines[].size` | `string` | no | Required when the item has sizes; one of its sizes |
| `lines[].modifiers` | `object[]` | no | Option names exactly as search_menu lists them, without the price suffix |
| `lines[].modifiers[].name` | `string` | yes |  |
| `lines[].modifiers[].options` | `string[]` | no |  |
| `lines[].special_instructions` | `string` | no | Note for this item (max 280 chars) |
| `special_instructions` | `string` | no | Note for the whole order (max 500 chars) |
| `notify_email` | `boolean` | no | Email the payment link to customer.email (default true) |
| `notify_sms` | `boolean` | no | Text the payment link to customer.phone. Sent only once iJava has texting for AI orders switched on; the result says if it was skipped (default true) |

### Example

**Request**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "create_order",
    "arguments": {
      "location": "almaden",
      "customer": {
        "name": "Jordan Lee",
        "email": "jordan@example.com",
        "phone": "408-555-0100"
      },
      "lines": [
        {
          "item_id": "cold-brew",
          "quantity": 2,
          "modifiers": [
            {
              "name": "Milk",
              "options": [
                "Oat milk"
              ]
            }
          ]
        }
      ],
      "special_instructions": "Light ice"
    }
  }
}
```

**Result (the text of content[0], parsed)**

```json
{
  "order_number": "IJ-261003-4214BB",
  "payment_url": "https://checkout.stripe.com/c/pay/cs_live_…",
  "payment_link_expires_in_minutes": 30,
  "status": "pending_payment",
  "notifications": {
    "email": "sent",
    "sms": "sent"
  },
  "next_step": "We sent the payment link to the customer by email and text. Also give them payment_url. The order goes to the kitchen when they pay."
}
```

> **Important:** Confirm the items and the price with the customer before you call this tool. The customer pays on Stripe's page, so nothing is charged until they do, and an unpaid order never reaches the kitchen.

Prices, tax and the pickup time are set by the server. The payment page shows the final total with tax. The link stays valid for 30 minutes.

### Email and text

By default the server sends the payment link to the customer by email and by text, so they can pay from their phone even if they never see your app. Set `notify_email` or `notify_sms` to `false` to skip one, for example when your app shows the link itself.

| notifications value | Meaning |
| --- | --- |
| `sent` | On its way to the customer. |
| `off` | You set it to `false`. |
| `skipped: …` | Not sent, with the reason. For example, texting for AI orders is not switched on yet, the number replied STOP, or this email or phone already had 3 AI orders in the last hour. |
| `failed` | The send did not go through. The order is fine; hand over `payment_url` yourself. |

The messages hold only the order number, the items, the cafe, the pickup time and the link. The customer's name and your notes are never included.

## Tool: `get_order_status`

_Check an order. Read-only._

Status, pickup time, items and total for an order. Needs the order number and the email it was placed with.

### Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `order_number` | `string` | yes | e.g. IJ-261002-A1B2C3, returned by create_order (max 40 chars) |
| `email` | `string` | yes | The email the order was placed with (email) |

### Example

**Request**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_order_status",
    "arguments": {
      "order_number": "IJ-261002-4214BB",
      "email": "jordan@example.com"
    }
  }
}
```

**Result (the text of content[0], parsed)**

```json
{
  "order_number": "IJ-261002-4214BB",
  "status": "paid",
  "location": "almaden",
  "cafe": "iJava Cafe — Willow Glen",
  "address": "2306 Almaden Rd #150, San Jose, CA 95125",
  "pickup": "ASAP",
  "items": [
    {
      "name": "Cold Brew",
      "quantity": 2,
      "price": "$15.50"
    }
  ],
  "subtotal": "$15.50",
  "total_paid": "$16.95"
}
```

| status | Meaning |
| --- | --- |
| `pending_payment` | Link created, the customer has not paid yet. |
| `paid` | Paid. The order is with the cafe. `refunded` appears if staff refunded any of it. |
| `payment_failed` | Checkout could not start or did not complete. |
| `canceled` | Staff canceled the order. |

A wrong email and an unknown order number get the same answer, `No order matches that number and email.`

## REST API overview

For your own app or service: order from iJava with a key, from a prepaid balance, with no customer in the loop. Three resources, locations, menu and orders. A live order is paid from your credits and prints in the kitchen at Delmas or Willow Glen the moment you place it. Pickup only; customers walk in.

|  |  |
| --- | --- |
| Base URL | `https://ijavacafe.com/api/v1` |
| Format | JSON in and out. Money is integer cents in USD. Times are ISO 8601; the cafes run on `America/Los_Angeles`. |
| Authentication | `Authorization: Bearer ijava_live_…` or `ijava_test_…`, from the [developer console](/ai/console). |
| Payment | Prepaid credits, like an AI token API: load a balance by card, every live order draws it down, and auto top-up refills it below a threshold you set. The MCP server, by contrast, never charges a card itself. |

1. [Create an account](/ai/console) with your email and make a **test** key.
2. Read the menu, place a test order, look at the response.
3. Add credits on the Billing page (the first load comes with $5 on us), make a **live** key, and ship.

**First call**

```bash
curl https://ijavacafe.com/api/v1/me \
  -H "Authorization: Bearer ijava_test_…"
```

### Test and live keys

| Key | Validates and prices | Charges your card | Prints in the kitchen | Shows up in |
| --- | --- | --- | --- | --- |
| `ijava_test_…` | Yes, identically | No | No | Your console, test list |
| `ijava_live_…` | Yes | Draws down your credits at placement | Yes, within seconds | Kitchen, kiosk, admin, your console |

Test orders go through the same pricing, modifier validation, sold-out checks and totals as live ones. The only differences: nothing is charged, nothing prints, and `status` is always `paid`.

## Keys, credits and GET /me

Send your key as a bearer token on every request. The middle word of the key is its mode. A key is shown once when you create it; revoke it in the console and it stops working on the next request.

```http
Authorization: Bearer ijava_live_4f1c…
```

> **Important:** Keep keys on a server. A key in a browser or a mobile app is a key anyone can use to buy breakfast on your card.

### Credits and auto top-up

Load credits on the console's Billing page, $5 to $2,000 at a time, paid on Stripe's page; the card you pay with is saved. Each live order draws menu price plus San Jose sales tax from the balance, no platform fee. There is no per-order cap: an order is refused only when the balance cannot cover it (`insufficient_balance`).

Switch on **auto top-up** and set two numbers: a threshold and a top-up amount. When an order would take the balance under the threshold, the saved card loads the top-up amount first, in the same request, and the order goes through. An order bigger than the top-up amount loads the shortfall plus the amount. A declined card only fails the order when the balance could not have covered it alone.

Refunds and cancellations happen at the counter and come straight back as credit. The ledger on the Billing page shows every movement with the balance after it.

### `GET /me`

```bash
curl https://ijavacafe.com/api/v1/me -H "Authorization: Bearer ijava_test_…"
```

**200 OK**

```json
{
  "object": "account",
  "id": "a1c2…",
  "name": "Sam Rivera",
  "company": "Breakfast Bot",
  "email": "sam@example.com",
  "key": {
    "id": "…",
    "name": "Breakfast bot",
    "mode": "test",
    "prefix": "ijava_test_ab12cd34"
  },
  "balance": {
    "cents": 8420,
    "auto_top_up": {
      "enabled": true,
      "threshold_cents": 2500,
      "amount_cents": 10000
    }
  },
  "billing": {
    "card_on_file": true,
    "card": {
      "brand": "visa",
      "last4": "4242"
    }
  }
}
```

## GET /locations

The two cafes you can order from. `accepting_orders` is the operator's switch: when it is `false`, live orders for that cafe return `ordering_paused`. `next_pickup_at` is `null` when an order placed now is made right away; otherwise it is the next opening it would be scheduled for.

**200 OK**

```json
{
  "object": "list",
  "data": [
    {
      "id": "almaden",
      "object": "location",
      "name": "iJava Cafe — Willow Glen",
      "short_name": "Willow Glen",
      "address": "2306 Almaden Rd #150, San Jose, CA 95125",
      "phone": "(408) 979-0251",
      "timezone": "America/Los_Angeles",
      "hours": {
        "mon": [
          {
            "open": "08:00",
            "close": "14:00"
          }
        ],
        "…": []
      },
      "open_now": true,
      "accepting_orders": true,
      "next_pickup_at": null,
      "kitchen_printer": "online"
    },
    {
      "id": "delmas",
      "…": "…"
    }
  ]
}
```

## GET /locations/{location}/menu

Everything orderable at one cafe, with the `id` you pass when ordering. Prices are `flat` or `sized`; for a sized item pass the size `label` as `size` on the order line, for example `"size": "Bowl"` on the tomato bisque.

Modifiers: `selection` is `one` (pick exactly one), `count` (pick exactly `count`), or `many` (any number, at most `max` when set). `one` and `count` modifiers are required. Option prices are added per option; a modifier's own `price_cents` is added once when it is used.

`available` is `false` when the cafe has marked an item sold out today. Ordering it returns `item_unavailable`.

**200 OK (abridged)**

```json
{
  "object": "menu",
  "location": "almaden",
  "categories": [
    {
      "id": "breakfast",
      "title": "Breakfast",
      "sections": [
        {
          "id": "burritos",
          "title": "Burritos",
          "items": [
            {
              "id": "fully-loaded-burrito",
              "name": "Fully Loaded Burrito",
              "description": "Beef bacon, eggs, cheddar jack, salsa, potatoes, spinach, avocado, chipotle sour cream",
              "price": {
                "kind": "flat",
                "cents": 1900
              },
              "modifiers": [
                {
                  "name": "Side",
                  "selection": "one",
                  "required": true,
                  "count": null,
                  "max": null,
                  "price_cents": 0,
                  "options": [
                    {
                      "value": "Salad",
                      "price_cents": 0
                    },
                    {
                      "value": "Fruit",
                      "price_cents": 0
                    },
                    {
                      "value": "No side",
                      "price_cents": 0
                    }
                  ]
                },
                {
                  "name": "Extras",
                  "selection": "many",
                  "required": false,
                  "count": null,
                  "max": 9,
                  "price_cents": 0,
                  "options": [
                    {
                      "value": "Extra avocado",
                      "price_cents": 200
                    },
                    {
                      "value": "…",
                      "price_cents": 0
                    }
                  ]
                }
              ],
              "flags": [
                "popular"
              ],
              "station": "food",
              "available": true
            }
          ]
        }
      ]
    }
  ]
}
```

## POST /orders

One request places, charges and sends the order. The response is the order as the kitchen sees it. Send an `Idempotency-Key` header (or `idempotency_key` in the body) and a retry of the same request returns the order you already placed, with `Idempotent-Replayed: true`, instead of a second charge.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `location` | `"delmas" \| "almaden"` | yes | The cafe. |
| `customer.name` | `string` | yes | Who picks it up. Goes on the ticket. |
| `customer.email` | `string` | yes | Gets the confirmation email. |
| `customer.phone` | `string` | yes | Staff call it if something is wrong. |
| `items` | `object[]` | yes | 1–40 lines, 40 items in total. |
| `items[].item_id` | `string` | yes | From the menu. |
| `items[].quantity` | `integer` | yes | 1–20. |
| `items[].size` | `string` | no | Required for sized prices: the size label. |
| `items[].modifiers` | `object[]` | no | `{ name, options[] }` per modifier used. Required modifiers must be present. |
| `items[].note` | `string` | no | Up to 280 characters, printed on the ticket. |
| `note` | `string` | no | Order-level note, up to 500 characters. |
| `idempotency_key` | `string` | no | Alternative to the header. |

**Request**

```bash
curl https://ijavacafe.com/api/v1/orders \
  -H "Authorization: Bearer ijava_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sam-breakfast-2026-10-08" \
  -d '{
    "location": "almaden",
    "customer": { "name": "Sam Rivera", "email": "sam@example.com", "phone": "+14085550100" },
    "items": [
      { "item_id": "fully-loaded-burrito", "quantity": 1,
        "modifiers": [{ "name": "Side", "options": ["Fruit"] },
                      { "name": "Extras", "options": ["Extra avocado"] }] },
      { "item_id": "latte", "quantity": 1,
        "modifiers": [{ "name": "Temperature", "options": ["Iced"] },
                      { "name": "Milk", "options": ["Oat milk"] }] }
    ],
    "note": "Pulling in at 8:40."
  }'
```

**201 Created**

```json
{
  "id": "6c1f1d0e-…",
  "object": "order",
  "mode": "live",
  "order_number": "IJ-261008-3F9A2C",
  "status": "paid",
  "location": "almaden",
  "pickup": {
    "at": null,
    "label": "ASAP"
  },
  "customer": {
    "name": "Sam Rivera",
    "email": "sam@example.com",
    "phone": "+14085550100"
  },
  "items": [
    {
      "item_id": "fully-loaded-burrito",
      "name": "Fully Loaded Burrito",
      "quantity": 1,
      "size": null,
      "modifiers": [
        {
          "name": "Side",
          "options": [
            "Fruit"
          ],
          "price_cents": 0
        },
        {
          "name": "Extras",
          "options": [
            "Extra avocado"
          ],
          "price_cents": 200
        }
      ],
      "note": null,
      "unit_cents": 2100,
      "line_cents": 2100,
      "station": "food"
    },
    {
      "item_id": "latte",
      "name": "Latte",
      "quantity": 1,
      "size": null,
      "modifiers": [
        {
          "name": "Temperature",
          "options": [
            "Iced"
          ],
          "price_cents": 0
        },
        {
          "name": "Milk",
          "options": [
            "Oat milk"
          ],
          "price_cents": 75
        }
      ],
      "note": null,
      "unit_cents": 675,
      "line_cents": 675,
      "station": "drinks"
    }
  ],
  "note": "Pulling in at 8:40.",
  "subtotal_cents": 2775,
  "tax_cents": 260,
  "total_cents": 3035,
  "refunded_cents": 0,
  "payment": {
    "method": "balance",
    "balance_after_cents": 5385
  },
  "kitchen": {
    "printed_at": null
  },
  "created_at": "2026-10-08T15:40:12.000Z",
  "paid_at": "2026-10-08T15:40:13.000Z"
}
```

**Payment.** `payment.balance_after_cents` is what is left on your credits once this order is paid; it is `null` when you read the order back later. `payment.method` is `test` for sandbox orders.

**Pickup timing.** During opening hours, with at least 30 minutes before close, an order is made right away and `pickup.at` is `null`. Otherwise it is scheduled for the next opening and `pickup.at` says when. Both cases print; a scheduled order prints with its time.

**The kitchen.** `kitchen.printed_at` is set by the cafe's printer box once the ticket has come out, usually within ten seconds. Poll the order to see it.

**Emails.** The customer gets a confirmation from iJava with the order number and pickup time. Staff get the ticket by email as well, marked with your account name.

## GET /orders/{id} and GET /orders

`GET /orders/{id}` returns one order with its current `status`: `paid`, `part_refunded`, `refunded`, `canceled` or `payment_failed`. Refunds and cancellations are done by staff at the counter and show here, with `refunded_cents` for the amount sent back to your card.

`GET /orders?limit=20` lists your account's orders in the key's mode, newest first, up to 100.

```bash
curl "https://ijavacafe.com/api/v1/orders?limit=5" -H "Authorization: Bearer ijava_live_…"
```

## Webhooks

Register an endpoint in the console (Webhooks tab) and we POST a signed JSON event when something happens to one of your orders. The kitchen and counter events come from the cafe itself: the printer box stamps the ticket, staff refund or cancel at the kiosk, and the event follows within about 15 seconds.

| Event | When |
| --- | --- |
| `order.paid` | The order was paid from your balance and sent to the kitchen. Sent at once. |
| `order.printed` | The kitchen printer box has printed the ticket. |
| `order.refunded` | Staff refunded some or all of it at the counter. Sent once per refund; `refunded_cents` is the running total. |
| `order.canceled` | Staff canceled the order. |

**Delivery body**

```json
{
  "id": "evt_5f0c…",
  "object": "event",
  "type": "order.printed",
  "created_at": "2026-10-08T15:40:21.000Z",
  "data": {
    "order": {
      "id": "6c1f1d0e-…",
      "order_number": "IJ-261008-3F9A2C",
      "status": "paid",
      "kitchen": {
        "printed_at": "2026-10-08T15:40:19.000Z"
      },
      "…": "…"
    }
  }
}
```

### Verifying a delivery

Every delivery carries `X-iJava-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is HMAC-SHA256 of `"<t>.<raw body>"` with your endpoint's secret. Compute it over the raw bytes, compare in constant time, and reject timestamps older than five minutes. `X-iJava-Event` names the type and `X-iJava-Delivery` is unique per attempt.

**Node.js**

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, rawBody, header) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
```

### Retries

Answer with any 2xx within 10 seconds. Anything else is retried with backoff — 1 minute, 5, 15, 30, then 1, 2, 6 and 12 hours — eight tries in all. An endpoint that fails 100 deliveries in a row is switched off; the console shows every delivery and lets you switch it back on. Deliveries can arrive out of order and, rarely, twice: key on `id`.

“Send test” in the console posts a sample `order.paid` with `"test": true` so you can check your verifier before the first real order.

## REST errors and limits

Every error is the same shape, with a stable `code` to branch on.

**422 Unprocessable Entity**

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Choose a size for Tomato Bisque."
  }
}
```

| HTTP | code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Body did not parse or failed validation; `fields` lists what. |
| 401 | `unauthorized` | Missing, malformed, revoked or unknown key. |
| 402 | `insufficient_balance` | Not enough credits for this order, and auto top-up did not cover it. Nothing was ordered. |
| 402 | `payment_declined` | Auto top-up tried the saved card and it was declined. Nothing was ordered. |
| 404 | `not_found` | Unknown location or order. |
| 409 | `item_unavailable` | Something in the order is sold out today. |
| 422 | `invalid_request` | A line could not be priced: bad item, size or modifier. |
| 429 | `rate_limited` | Slow down; `Retry-After` says how long. |
| 503 | `ordering_paused` | The cafe's app-and-AI ordering switch is off right now. |
| 5xx | `server_error` | Our side. Retry with the same idempotency key. |

### Limits

- 120 read requests a minute and 20 order requests a minute per key.
- 40 items per order, 20 of any one line. No per-order dollar cap: the balance is the limit.
- Credits load from $5 to $2,000 at a time.
- Twenty active keys per account.
- Request bodies up to 32 KB.

## Errors

There are two kinds. A **tool error** means the tool ran and said no: bad arguments, a sold-out item, a closed cafe. It comes back as a normal result with `isError: true` and a sentence your model can read and act on.

**Tool error**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Complete Milk for Cold Brew."
      }
    ],
    "isError": true
  }
}
```

A **protocol error** means the request itself was wrong. It comes back as a JSON-RPC `error`.

| Code | HTTP | Meaning |
| --- | --- | --- |
| `-32700` | 400 | The body is not valid JSON. |
| `-32600` | 400 / 413 | Not a single JSON-RPC 2.0 message, or the body is over 64 KB. |
| `-32601` | 200 | Unknown method. The server supports `initialize`, `ping`, `tools/list` and `tools/call`. |
| `-32602` | 200 | Unknown tool name. |
| `-32000` | 429 | Rate limited. |

## Raw protocol

You only need this to call the server without an MCP client. Every request is a `POST` to the endpoint with one JSON-RPC message. There are no sessions: you can skip `initialize` and call `tools/call` directly.

**List the tools**

```bash
curl -s https://ijavacafe.com/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

**Call a tool**

```bash
curl -s https://ijavacafe.com/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_locations","arguments":{}}}'
```

The result of `tools/call` is `{ content: [{ type: "text", text }] }`, where `text` is JSON. `tools/list` returns each tool's full JSON Schema as `inputSchema`. Notifications get HTTP `202` with no body. `GET` and `DELETE` get `405`, because there is no event stream and no session to end. CORS is open, so a browser page can call the server directly.

## Changelog

| Date | Version | Change |
| --- | --- | --- |
| 2026-10-08 | 1.2.0 | REST API at `/api/v1` for apps that order from a prepaid balance: developer accounts, test and live keys, credits with auto top-up at `/ai/console`. Signed webhooks for paid, printed, refunded and canceled orders. Documented under REST API. |
| 2026-10-03 | 1.1.0 | `create_order` emails and texts the payment link to the customer by default, with `notify_email` and `notify_sms` to turn either off, and returns `notifications`. AI orders open at Willow Glen. |
| 2026-10-03 | 1.0.1 | Docs: the server is open by design. No API keys, customer accounts or saved cards are planned. The customer confirms every order by paying. |
| 2026-10-02 | 1.0.0 | First release: `list_locations`, `search_menu`, `create_order`, `get_order_status`. |
