track_shipment
track_shipment returns shipment tracking for an order. Like order_status, 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 → look up the shipment and 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 tracking data. ← BUG: attacker can now enumerate any tracking chain.The wrong pattern leaks a tracking chain (carrier, tracking number, expected delivery) to any caller who guesses an order number. Refuse it.
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”.
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.
Request
Section titled “Request”Machine-readable: /skills/v1/track_shipment.request.schema.json
| Field | Type | Required | Notes |
|---|---|---|---|
order_id | string | yes | 1–128 chars. |
customer_email | string | yes | 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": "track_shipment", "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/track_shipment.response.schema.json
Only order_id is required. Every other field is optional so you can honestly represent “the order exists but nothing has shipped yet” as { "order_id": "#1001" }.
| Field | Type | Required | Notes |
|---|---|---|---|
order_id | string | yes | Echo the order id. 1–256 chars. |
tracking_number | string | no | Carrier-issued tracking number. 1–256 chars. |
carrier | string | no | Carrier name / code. 1–256 chars. |
status | string | no | Free-form status. 1–64 chars. not_yet_shipped, in_transit, delivered, pending, cancelled, or a carrier-specific string. |
expected_delivery | string | no | ISO-8601 UTC. 20–40 chars. |
tracking_url | string | no | HTTPS URL to the carrier’s tracking page. 1–2048 chars. |
Do NOT include the customer name, shipping address, phone, or other customer-scoped detail — additionalProperties: false rejects them as malformed_response. Minimum disclosure by construction.
{ "additionalProperties": false, "properties": { "carrier": { "maxLength": 256, "minLength": 1, "type": "string" }, "expected_delivery": { "maxLength": 40, "minLength": 20, "type": "string" }, "order_id": { "maxLength": 256, "minLength": 1, "type": "string" }, "status": { "maxLength": 64, "minLength": 1, "type": "string" }, "tracking_number": { "maxLength": 256, "minLength": 1, "type": "string" }, "tracking_url": { "maxLength": 2048, "minLength": 1, "type": "string" } }, "required": ["order_id"], "type": "object"}Example response — shipment in transit
Section titled “Example response — shipment in transit”{ "order_id": "#1001", "tracking_number": "1Z999AA10123456784", "carrier": "UPS", "status": "in_transit", "expected_delivery": "2026-07-08T00:00:00.000Z", "tracking_url": "https://www.ups.com/track?tracknum=1Z999AA10123456784"}Example response — not yet shipped
Section titled “Example response — not yet shipped”{ "order_id": "#1001", "status": "not_yet_shipped"}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.
Do NOT return a canonical response with a fabricated status — 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: false— noshipping_address, nocustomer_name. - Bounds.
tracking_urlup to 2048 chars;statusup to 64.
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.
order_status— the sibling customer-scoped skill.