Verifying live
A green test suite proves your handler works. It does not prove the gateway routes to it, that the install wrote your routes, or that your declared contract accepts the requests you meant it to. Those need real calls.
Do this on staging first, then repeat it on production after that deploy.
The four checks
curl -i https://api.revenexx.com/v1/serials/ \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT"
The health route answers without a tenant identity, so a 200 here means the app is deployed and reachable at all. A 404 means the install step did not run for this version.
curl -s -X POST https://api.revenexx.com/v1/serials/defaults \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "Content-Type: application/json" \
-d '{}'
curl -s "https://api.revenexx.com/v1/serials?limit=50&order=created_at.desc" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT"
curl -s https://api.revenexx.com/v1/openapi.json \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
| grep -o '"/serials[^"]*"'
Step 4 is the one people skip and the one that catches the most. See The published OpenAPI document.
Always send {} on a POST with a request schema
A POST with no body at all fails the gateway's contract validation before your function is invoked, when the capability declares a request schema. Send {} if you have nothing to say:
curl -X POST https://api.revenexx.com/v1/serials/defaults \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
The failure is confusing because your handler is never reached and nothing appears in your execution log: it looks like your app rejected the call, and your app never saw it. If you get a 400 on a POST you are sure about, check the body and the Content-Type before you check your code.
The same mechanism is behind the stricter-contract trap — validation happens in front of you, so a 400 you cannot explain locally usually means the contract, not the handler.
Call /defaults explicitly
Do not rely on app.installed to have seeded anything. It does not reliably fire on a Marketplace install.
Expose an idempotent /defaults capability, seed on the event as well, and call the route once after every install. Every shipped app does exactly this; it is not a workaround for a bug you can wait out, it is the reliable path.
Because it is idempotent, calling it twice is free — so put it in your deploy script rather than in a runbook nobody reads:
revenexx deploy app --owner "$REVENEXX_TENANT"
curl -s -X POST "https://api.revenexx.com/v1/serials/defaults" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "Content-Type: application/json" -d '{}'
Quote the request ID
Every response carries an X-Request-ID; the gateway generates one if you did not send your own. Use -i to see it:
curl -i "https://api.revenexx.com/v1/serials?limit=1" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT"
It is the correlation id for the request end to end, so:
- Log it on the client side for anything you might have to ask about later.
- Quote it in a support request. It is what makes "it failed at 14:20" answerable.
- Forward it when your app calls another app, so one trace spans the whole chain.
Two other response headers worth reading while you are here: X-Capability-App names the app that served the capability — which is how you confirm an override is in the path — and X-Cache tells you whether a cacheable response was a HIT. See API usage.
Then check the parts curl cannot
- Open the app in the Cockpit. Views render, existing data survived, no empty screens from a mistyped column name. See How the Cockpit renders.
- Read the executions. Every call you just made should be there, with the status you expected. A call you made that is not there was rejected before it reached you. See Operating.
- Check the settings form. The keys you declared, grouped, with sensible defaults.
- Wait for a schedule, or wait for the next tick. A cron job starts firing once the version declaring it is installed.
Reading a failure
| You get | Usually |
|---|---|
404 on your path | The install did not run for this version, or the app is not installed on this tenant |
400 on a POST you are sure about | No body, or a contract stricter than your handler |
403 on a write | The entity grant does not cover that operation |
401 | The credential, not your app |
| Same fifty rows on every page | The list capability does not declare limit/offset |
502 | Your function threw, or something it called did. Read the execution |