Self-test — the required final step before going live
Before you enable your fulfillment endpoint for live agent traffic, run this self-test. Sill will run its own hookup checks at the moment you save your endpoint URL, but there is one check Sill CANNOT do from the outside: verify that a request for a real order id and the WRONG customer email refuses to return the order. That check requires your customer database — nobody but you can decide what “wrong email” means.
Everything below runs on your machine. Sill never sees your test order, your customer’s email, or your endpoint’s response bytes.
What Sill checks at hookup
Section titled “What Sill checks at hookup”When you save an endpoint URL in the dashboard, Sill runs three checks before the skill starts advertising to agents:
- Reachability — Sill sends a
GETto your endpoint and expects any HTTP 2xx, 3xx, or 4xx. A network error or a 5xx blocks the skill. - Signature enforcement — Sill sends two
POSTrequests to your endpoint using a canonical, PII-free probe body: one with theX-Sill-Signatureheader OMITTED, one with a well-formed but wrong-secret HMAC. Your endpoint MUST reject both with a non-2xx. If either is accepted, any caller who can reach your URL can forge a call to your handler, and the skill is blocked. - Response shape — Sill sends one correctly-signed probe with a marker order id that does not exist in your database, and expects a response that validates against the canonical shape on the
order_statuspage (and the sibling pages for the other four read skills you have attached). An over-sharing shape — say, acustomer_addressfield onorder_status— is caught here, before real customer data flows.
These three checks catch the loudest, most common integration mistakes. They do NOT verify that your handler correctly refuses a real order id when the customer email does not match — that check needs real data. This page tells you how to run it yourself.
What ONLY you can check
Section titled “What ONLY you can check”Sill has no way to know which email owns which order in your database, so the following list is on you. Each item is a common way an otherwise-correct handler still leaks data. Run every check locally with real order data before you go live.
- Real order id, WRONG customer email → not the order. This is the customer-scoping check (BOLA). Take a real order id in your database and pair it with any email that does NOT own it. Your handler MUST refuse. The recommended response for
order_statusandtrack_shipmentisHTTP 404with an empty body. - Real order id, RIGHT customer email → the canonical shape ONLY. Return the fields the canonical response schema lists and nothing else. Do NOT include the customer’s name, shipping address, phone, internal notes, payment method, or any other field. Sill’s response validator will reject over-shares with
additionalProperties: false, but you should catch them at the source. - Sequential order ids don’t leak existence. A caller who guesses
#1001,#1002,#1003should see the same response shape whether or not each id exists. Do NOT distinguish “order not found” from “wrong customer” — both should be indistinguishable. The recommended answer for both isHTTP 404with an empty body. - Wrong email → not-found response takes similar time as a real-email response. A handler that returns instantly on wrong-email and takes 200ms on right-email leaks existence through a timing side channel. Compare the argument email to the stored customer email in constant time (
crypto.timingSafeEqualin Node,hmac.compare_digestin Python,hmac.Equalin Go). - One email with multiple orders returns the RIGHT one. If a customer has multiple orders in your database, a call for
order_id: A, customer_email: XwhereXalso ownsBandCshould return onlyA. Do not aggregate. - The signed request’s timestamp is enforced. Reject a correctly-signed request whose
tvalue is more than about five minutes off from your server’s clock. The contract overview has the exact snippet.
Curl one-liner — signed request
Section titled “Curl one-liner — signed request”Use this to make ONE correctly-signed request against your endpoint by hand. Substitute the secret and the endpoint URL from your dashboard, and the order id + email from your test data.
SECRET='whs_...paste_from_dashboard...'ENDPOINT='https://shop.example.com/sill/skills'BODY='{"skill_id":"order_status","site_id":"01K00000000000000000000000","arguments":{"order_id":"#1001","customer_email":"buyer@example.com"},"observed_at":"2026-07-07T00:00:00.000Z","nonce":"selftest0000000000000000000000"}'T=$(date +%s)SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')curl -sSi -X POST "$ENDPOINT" \ -H 'content-type: application/json' \ -H "x-sill-signature: t=$T,v1=$SIG" \ --data "$BODY"To exercise the wrong-email path, change the customer_email in $BODY to any email that does NOT own #1001 in your database, recompute $SIG, and re-run. The response should be HTTP 404 with an empty body.
Automated check — sill-endpoint-check.mjs
Section titled “Automated check — sill-endpoint-check.mjs”Sill publishes a single-file Node script that runs the same three hookup checks against your endpoint AND the customer-scoping check that only you can meaningfully run. Zero npm dependencies, Node 18+ built-ins only, works offline.
- Download:
/skills/v1/sill-endpoint-check.mjs - Run: below.
curl -sSLo sill-endpoint-check.mjs https://docs.sill.so/skills/v1/sill-endpoint-check.mjs
SILL_ENDPOINT_URL="https://shop.example.com/sill/skills" \SILL_SKILL_SECRET="whs_...paste_from_dashboard..." \SILL_TEST_ORDER_ID="#1001" \SILL_TEST_ORDER_EMAIL="buyer@example.com" \SILL_TEST_WRONG_EMAIL="notmine@example.com" \ node sill-endpoint-check.mjsEnvironment variables
Section titled “Environment variables”| Variable | Notes |
|---|---|
SILL_ENDPOINT_URL | Your HTTPS fulfillment endpoint — the URL you registered in the dashboard for order_status. http://localhost:... is accepted for testing against a local staging instance. |
SILL_SKILL_SECRET | The shared secret Sill showed you once when you saved the skill. It starts with whs_. |
SILL_TEST_ORDER_ID | A real order id in your database. Test-mode or sandbox orders are fine. The script sends it twice — once with the right email, once with a wrong one. |
SILL_TEST_ORDER_EMAIL | The customer email that owns SILL_TEST_ORDER_ID in your database. |
SILL_TEST_WRONG_EMAIL | Any email that does NOT own SILL_TEST_ORDER_ID. The customer-scoping check pairs this with the real order id. |
SILL_SITE_ID | Optional. Your site’s ULID. Defaults to a placeholder. If your handler ignores site_id you can leave this unset. |
SILL_REQUEST_TIMEOUT_MS | Optional. Per-request timeout. Defaults to 10000. |
What the script checks
Section titled “What the script checks”- Reachability —
GETto your endpoint returns any 2xx / 3xx / 4xx. - Signature: unsigned request — a
POSTwith theX-Sill-Signatureheader omitted is REJECTED (any non-2xx). - Signature: wrong-secret request — a
POSTwith a well-formed but wrong-key HMAC is REJECTED. - Happy path — a correctly-signed
POSTwith your real order id and right email returnsHTTP 200and a body that matches the canonicalorder_statusresponse schema (additionalProperties: falseat every depth). - Customer scoping — a correctly-signed
POSTwith your real order id and the WRONG email does NOT return the order. The script prints a loud[FAIL]if the response body contains line items for a wrong-email request — that is a customer-scoping leak (BOLA).
Reading the output
Section titled “Reading the output”Each check prints a single line: [PASS], [WARN], or [FAIL], the check name, and a short reason. The script never prints your secret and never prints your endpoint’s raw response body.
[PASS]— the check passed.[WARN]— the check surfaced something recommended-to-fix but not a hard block. The most common[WARN]is a wrong-email response that returnsHTTP 200with an empty body instead ofHTTP 404with an empty body — no data was leaked, but a distinguishable “wrong email” response tells the caller the order id exists.[FAIL]— a check that must be fixed before going live. The script exits non-zero.
Exit codes:
0— every check passed or only produced warnings.1— at least one check failed.2— the configuration is missing or invalid.
The check the script can’t do without your help
Section titled “The check the script can’t do without your help”The customer-scoping check depends on SILL_TEST_WRONG_EMAIL genuinely NOT being the owner of SILL_TEST_ORDER_ID in your database. If you accidentally set it to another email that DOES own the order (an alt email on the same customer, for example), the check might pass on a leaky endpoint. Pick a wrong email that no customer in your database uses.
Once every check passes
Section titled “Once every check passes”You are ready to go live. The dashboard’s skill row will show the results of Sill’s own hookup checks — the ones you already saw in this script’s first three lines. The customer-scoping check is the one Sill cannot re-run: it stays your responsibility on every code change to your handler.
If you change your handler, re-run this self-test. It takes about five seconds and it is the cheapest way to catch a regression before real agent traffic hits your endpoint.
See also
Section titled “See also”- Contract overview — envelope shape, HMAC signing, and full verification samples in TypeScript / Python / Go.
order_status— the canonical response shape the script validates against.track_shipment— the other customer-scoped skill. Same discipline; re-run the checklist above with atrack_shipmentrequest body.