Calling another app

How your app reaches another app's capability through the gateway — the capability grant, the headers to forward, and telling your own 4xx from their 502.

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:

manifest.json
"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:

src/main.js
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

HeaderWhy
X-Revenexx-TenantWhich tenant the call applies to. Forward the one you were called with — never a tenant from a request body.
X-Revenexx-ContextThe 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-IDCorrelation. 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:

src/main.js
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 backIt meansYou answer
400 from the other appYou built a bad request — usually a field you passed through unvalidated400, with a message about the field your caller sent
403Your app lacks the capability grant, or the acting principal may not do this403 — and check your manifest before you blame the tenant
404The referenced record does not exist for this tenantOften 400 ("no product with that id") if the caller supplied the id, or 404 if the missing thing is the resource
409The other app refused on state409, with their meaning translated into yours
429You are calling too muchBack off; do not convert it into a 500
502The other app or something behind it failedLet 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 wantUse
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 elsewhereAn event you receive. Asynchronous, and the publisher does not know you exist.
To reach an external system, with retries and visibilityAn Integration Studio workflow, or a declared outbound host.
Another app's id resolved to a human label in a formA lookup, which needs no call from you at all.

Next steps

Was this page helpful?