order_status
order_status looks up an order’s status and line items. It is customer-scoped: Sill sends customer_email as a required argument, and your handler MUST verify that email belongs to the order before returning any data.
Customer scoping — your responsibility
Section titled “Customer scoping — your responsibility”Sill cannot enforce this for you. Only your customer database knows which email owns which order.
Correct
Request: { order_id: "#1001", customer_email: "[email protected]" }
Handler: 1. Look up order #1001. 2. Compare its customer email (in constant time) to "[email protected]". 3. If they match → return the canonical result. 4. If they don't match, or order does not exist → return HTTP 404 with an empty body.Wrong
Request: { order_id: "#1001", customer_email: "[email protected]" }
Handler (missing the check): 1. Look up order #1001. 2. Return its data. ← BUG: [email protected] never owned this order.The wrong pattern is the same class of bug as writing your own storefront order-lookup form and forgetting the customer-scoping check — an agent that guesses an order number can then read any order.
The check
Section titled “The check”Compare the email argument to your stored customer email in constant time, lowercased and NFKC-normalized on both sides. In Node.js:
import { timingSafeEqual } from 'node:crypto';
function emailsMatch(a: string, b: string): boolean { const na = a.trim().toLowerCase().normalize('NFKC'); const nb = b.trim().toLowerCase().normalize('NFKC'); const ba = Buffer.from(na, 'utf8'); const bb = Buffer.from(nb, 'utf8'); if (ba.length !== bb.length) return false; return timingSafeEqual(ba, bb);}If the emails do not match, respond HTTP 404 with an empty body. Do NOT distinguish “order not found” from “wrong customer” — both leak information about which orders exist.
A 404 is a normal, expected response — it is treated as a clean “not found” and returned to the agent as such. It does not count against your endpoint’s health. Reserve non-404 error statuses (5xx, timeouts) for genuine failures; those are what mark an endpoint unhealthy.
Request
Section titled “Request”Machine-readable: /skills/v1/order_status.request.schema.json
| Field | Type | Required | Notes |
|---|---|---|---|
order_id | string | yes | Merchant-facing order name (e.g. #1001) or your internal id. 1–128 chars. |
customer_email | string | yes | The email the caller claims owns the order. 3–320 chars. |
{ "additionalProperties": false, "properties": { "customer_email": { "maxLength": 320, "minLength": 3, "type": "string" }, "order_id": { "maxLength": 128, "minLength": 1, "type": "string" } }, "required": ["order_id", "customer_email"], "type": "object"}Example request
Section titled “Example request”{ "skill_id": "order_status", "site_id": "01EXAMPLE00000000000000000", "arguments": { "order_id": "#1001", }, "observed_at": "2026-07-05T18:22:15.140Z", "nonce": "01K1EXAMPLE0000000000000000"}Response
Section titled “Response”Machine-readable: /skills/v1/order_status.response.schema.json
| Field | Type | Required | Notes |
|---|---|---|---|
order_id | string | yes | Echo the order id. 1–256 chars. |
status | enum | yes | pending | paid | fulfilled | cancelled | refunded. |
placed_at | string | yes | ISO-8601 UTC of when the order was placed. 20–40 chars. |
fulfilled_at | string | no | ISO-8601 UTC of fulfillment, when applicable. |
line_items | array | yes | Up to 128 items. Each has sku, title, quantity, unit_price. |
The canonical response is minimum disclosure. Do NOT include the customer’s name, shipping address, phone, internal notes, or any other detail — additionalProperties: false at every object depth rejects them with malformed_response.
Each line item:
| Field | Type | Required | Notes |
|---|---|---|---|
sku | string | yes | 1–256 chars. |
title | string | yes | 1–512 chars. |
quantity | integer | yes | Non-negative. |
unit_price | object | yes | { amount, amount_decimal, currency } — same shape as browse_catalog. |
{ "additionalProperties": false, "properties": { "fulfilled_at": { "maxLength": 40, "minLength": 20, "type": "string" }, "line_items": { "items": { "additionalProperties": false, "properties": { "quantity": { "minimum": 0, "type": "integer" }, "sku": { "maxLength": 256, "minLength": 1, "type": "string" }, "title": { "maxLength": 512, "minLength": 1, "type": "string" }, "unit_price": { "additionalProperties": false, "properties": { "amount": { "type": "number" }, "amount_decimal": { "maxLength": 64, "minLength": 1, "type": "string" }, "currency": { "maxLength": 8, "minLength": 3, "type": "string" } }, "required": ["amount", "amount_decimal", "currency"], "type": "object" } }, "required": ["sku", "title", "quantity", "unit_price"], "type": "object" }, "maxItems": 128, "type": "array" }, "order_id": { "maxLength": 256, "minLength": 1, "type": "string" }, "placed_at": { "maxLength": 40, "minLength": 20, "type": "string" }, "status": { "enum": ["pending", "paid", "fulfilled", "cancelled", "refunded"], "type": "string" } }, "required": ["order_id", "status", "placed_at", "line_items"], "type": "object"}Example response
Section titled “Example response”{ "order_id": "#1001", "status": "fulfilled", "placed_at": "2026-06-30T14:12:03.000Z", "fulfilled_at": "2026-07-02T09:41:18.000Z", "line_items": [ { "sku": "beans_medium_12oz", "title": "Medium roast, 12 oz bag", "quantity": 2, "unit_price": { "amount": 1800, "amount_decimal": "18.00", "currency": "USD" } } ]}Not-found response
Section titled “Not-found response”For any of these conditions, return HTTP 404 with an empty body:
- Order does not exist.
- Email does not match the order’s stored customer email.
- Order exists but is soft-deleted / archived.
Do NOT return a canonical response with placeholder values (empty line_items, status: "pending") — that leaks the existence of an order id you refused to reveal.
What Sill checks
Section titled “What Sill checks”- Extra fields are rejected.
additionalProperties: falseat every depth — nocustomer_address, nointernal_notes, nopayment_method. This is the minimum-disclosure gate; an over-sharing handler is caught at this boundary. statusoutside the enum. Only the five canonical values are accepted. Map your internal statuses to the closest canonical value.placed_atmalformed. Must be a 20–40 char ISO-8601 UTC string."2026-06-30T14:12:03Z"and"2026-06-30T14:12:03.000Z"both fit.
Verifying Sill’s signature
Section titled “Verifying Sill’s signature”Every request Sill POSTs to your endpoint carries a X-Sill-Signature: t=<unix>,v1=<hex> header, where <hex> is HMAC-SHA256("<t>.<raw-body>") with your shared secret.
TypeScript (compact)
Section titled “TypeScript (compact)”import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(header: string, body: string, secret: string): boolean { const map = Object.fromEntries(header.split(',').map((p) => p.split('=', 2))); const t = Number(map.t); const sig = String(map.v1 ?? ''); if (!Number.isFinite(t) || sig.length === 0) return false; if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false; const expected = createHmac('sha256', secret).update(`${t}.${body}`).digest(); const got = Buffer.from(sig, 'hex'); if (expected.length !== got.length) return false; return timingSafeEqual(expected, got);}Python (compact)
Section titled “Python (compact)”import hmac, hashlib, time
def verify(header: str, body: bytes, secret: bytes) -> bool: parts = dict(p.split('=', 1) for p in header.split(',') if '=' in p) try: t = int(parts['t']) except (KeyError, ValueError): return False sig = parts.get('v1', '') if abs(int(time.time()) - t) > 300 or not sig: return False expected = hmac.new(secret, f'{t}.'.encode() + body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, sig)Go (compact)
Section titled “Go (compact)”func Verify(header string, body []byte, secret []byte) bool { var t int64 = -1 var sig string for _, p := range strings.Split(header, ",") { kv := strings.SplitN(p, "=", 2) if len(kv) != 2 { continue } switch kv[0] { case "t": n, err := strconv.ParseInt(kv[1], 10, 64) if err != nil { return false } t = n case "v1": sig = kv[1] } } if t < 0 || sig == "" { return false } diff := time.Now().Unix() - t if diff < 0 { diff = -diff } if diff > 300 { return false } mac := hmac.New(sha256.New, secret) mac.Write([]byte(strconv.FormatInt(t, 10) + ".")) mac.Write(body) got, err := hex.DecodeString(sig) if err != nil { return false } return hmac.Equal(mac.Sum(nil), got)}See also
Section titled “See also”- Contract overview — envelope shape, versioning, and the full HMAC verification samples.
track_shipment— the sibling customer-scoped skill.