Calling another app
An app talks to another app through the gateway, not through its tables. There is no shared database, no cross-app foreign key, and no back channel. The boundary between two apps is their capabilities.
Declare the grant
You may only call a capability you have declared:
"permissions": [
{ "capability": "products.get", "compatible": "^1.0" },
{ "capability": "markets.list" }
]
capability is the dotted capability key of the operation you intend to call, not its URL. compatible is an optional semver range describing the contract version you built against.
The gateway resolves a compatible implementation at call time — which may be the platform app or a tenant's own override, and your code cannot tell the difference. That is the point: a tenant whose stock lives in an ERP swaps the implementation, and every app that calls inventories.availability keeps working unchanged.
Declare one entry per capability you actually call. An undeclared call is refused, and an entry you never exercise makes the merchant's consent screen harder to answer for no benefit. See Permissions.
An app you cannot run without also belongs in dependencies, so the merchant is never left with your app installed and its dependency missing. See Dependencies.
Make the call
You call the same public route a partner would, at the same gateway:
async function getProduct(c, productId) {
const res = await fetch(`https://api.revenexx.com/v1/products/${productId}`, {
headers: {
'X-Revenexx-Tenant': c.header('x-revenexx-tenant'),
'X-Revenexx-Context': c.header('x-revenexx-context'),
'X-Request-ID': c.header('x-request-id'),
'Accept': 'application/json',
},
});
if (res.status === 404) return null;
if (!res.ok) throw new Error(`products.get failed: ${res.status}`);
return res.json();
}
The headers to forward
| Header | Why |
|---|---|
X-Revenexx-Tenant | Which tenant the call applies to. Forward the one you were called with — never a tenant from a request body. |
X-Revenexx-Context | The identity context the gateway injected into your invocation. Forwarding it is what makes the call yours on behalf of this caller rather than an anonymous one. |
X-Request-ID | Correlation. Forwarding it is what makes one trace span the whole chain, and what makes a support ticket answerable. |
You forward context; you do not mint credentials. There is no API key for your app to hold, no token to refresh, and nothing to put in an environment variable — which is the same property the runtime adapter gives you for your own tables.
Do not forward a caller's Content-Type, Content-Length or Host. Do not forward a header the downstream capability does not declare — it will be ignored, and if the capability is cached it may pollute the cache key.
Ask once, not per item
A call across the gateway is a network hop with contract validation on both ends. In a loop over 200 order lines that is 200 hops.
Prefer a batch capability where the other app publishes one, and where it does not, read what you need in as few calls as possible and match in memory. When you genuinely need several unrelated calls, run them concurrently:
const [product, market] = await Promise.all([
getProduct(c, c.body.product_id),
listMarkets(c),
]);
Whose fault is it
The most useful discipline in cross-app code is deciding, for every failure, whether your caller can act on it.
| You get back | It means | You answer |
|---|---|---|
400 from the other app | You built a bad request — usually a field you passed through unvalidated | 400, with a message about the field your caller sent |
403 | Your app lacks the capability grant, or the acting principal may not do this | 403 — and check your manifest before you blame the tenant |
404 | The referenced record does not exist for this tenant | Often 400 ("no product with that id") if the caller supplied the id, or 404 if the missing thing is the resource |
409 | The other app refused on state | 409, with their meaning translated into yours |
429 | You are calling too much | Back off; do not convert it into a 500 |
502 | The other app or something behind it failed | Let it be a 502. Do not dress it as a 400. |
The line that matters: a 4xx is yours, a 502 is theirs. A 4xx means something in the request could be fixed by whoever made it — so translate it into a 4xx your caller can act on. A 502 means nothing your caller sends will change the outcome, so telling them their request was bad is a lie that costs them an afternoon.
Do not swallow a failure into an empty result either. A products.get that fails is not "no product" — a route that treats it as one turns an outage into silently wrong data.
Retrying
A 502 and a 429 are the retryable ones, with backoff. Retry a GET freely. Retry a POST only when the capability you are calling is idempotent, and assume it is not unless it says so. See Retries and idempotency.
Never retry a 400, a 403, a 404 or a 409 — the answer will not change.
When not to call at all
| You want | Use |
|---|---|
| An answer before you can continue — "is this in stock?", "what does this cost?" | A capability call. Synchronous, and you declare the grant. |
| To react to something that happened elsewhere | An event you receive. Asynchronous, and the publisher does not know you exist. |
| To reach an external system, with retries and visibility | An Integration Studio workflow, or a declared outbound host. |
| Another app's id resolved to a human label in a form | A lookup, which needs no call from you at all. |
Next steps
- Permissions — the
capabilitygrant among the others. - Overriding a capability — being the app on the other end.
- Errors — the statuses you answer with.
- API usage — the headers and conventions the call follows.