Custom API/Proxy request
The typed provider methods (getAccount, getNetworkConfig,
getTokenOfAccount, ...) cover the common endpoints. The API and gateway expose
many more. When you need one the SDK does not model, call it raw, both escape
hatches are on INetworkProvider:
doGetGeneric(resourceUrl), a GET, path relative to the provider's base URL.doPostGeneric(resourceUrl, payload), a POST.
Both return any, so you own the shape of the result. This recipe wraps
three raw calls, the economics and stats GETs, and a raw VM query POST, in
typed functions.
Prerequisites
- Node.js >= 20.19.0.
- Network access to devnet. No wallet, no PEM, no EGLD.
Install
mkdir custom-api-request
cd custom-api-request
# Create the project files shown on this page.
npm install
npm run build
npm start
Complete project files omitted from the main walkthrough
{
"name": "cookbook-recipe-custom-api-request",
"version": "1.0.0",
"private": true,
"description": "Cookbook recipe — call any API or Proxy endpoint the typed provider methods do not cover, via doGetGeneric and doPostGeneric (economics, stats, a raw VM query).",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit --strict"
},
"dependencies": {
"@multiversx/sdk-core": "15.4.1",
"axios": "1.18.1",
"bignumber.js": "9.3.1",
"protobufjs": "7.6.5"
},
"devDependencies": {
"@types/node": "20.19.43",
"typescript": "5.9.3"
},
"engines": {
"node": ">=20.19.0"
}
}
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"declaration": false,
"sourceMap": false,
"outDir": "dist",
"types": ["node"]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
The raw calls
// src/customRequest.ts — the subject of this recipe: the escape hatch. When a
// provider has no typed method for the endpoint you need, call it raw:
//
// doGetGeneric(resourceUrl) → any (a GET, path relative to the base URL)
// doPostGeneric(resourceUrl, payload) → any (a POST)
//
// Both are on INetworkProvider. They return `any`, so YOU own the shape of the
// result — the SDK does not validate it. This file wraps three raw calls in
// typed functions so the rest of the app gets real types back.
import type { INetworkProvider } from '@multiversx/sdk-core';
/**
* The API's `/economics` endpoint — total/circulating supply, staked, price,
* market cap, APR. There is NO typed provider method for it; raw is the only
* way. This interface is the shape you assert (you are responsible for it).
*/
export interface Economics {
totalSupply: number;
circulatingSupply: number;
staked: number;
price: number;
marketCap: number;
apr: number;
}
export async function getEconomics(provider: INetworkProvider): Promise<Economics> {
return (await provider.doGetGeneric('economics')) as Economics;
}
/** The API's `/stats` endpoint — another untyped GET. */
export interface NetworkStats {
shards: number;
blocks: number;
accounts: number;
transactions: number;
}
export async function getStats(provider: INetworkProvider): Promise<NetworkStats> {
return (await provider.doGetGeneric('stats')) as NetworkStats;
}
/**
* A raw VM query via POST — the same read `SmartContractController.query`
* wraps, shown at the HTTP level. `returnData` comes back as base64-encoded,
* big-endian bytes; this decodes the first return value to a bigint.
*/
export async function rawQuery(
provider: INetworkProvider,
scAddress: string,
funcName: string,
): Promise<bigint> {
const result = (await provider.doPostGeneric('query', {
scAddress,
funcName,
args: [],
})) as { returnCode: string; returnData: string[] };
const [first] = result.returnData;
if (first === undefined || first === '') {
return 0n;
}
const bytes = Buffer.from(first, 'base64');
return bytes.length === 0 ? 0n : BigInt(`0x${bytes.toString('hex')}`);
}
Wiring it up
// src/index.ts — three raw calls against devnet: two GETs the typed methods do
// not model (economics, stats) and one POST (a VM query) that they DO, to show
// the raw layer underneath. No wallet, no gas.
//
// Usage:
// npm run build && npm start
import { DevnetEntrypoint } from '@multiversx/sdk-core';
import { getEconomics, getStats, rawQuery } from './customRequest';
// The adder contract on devnet — the same stable fixture the query recipe uses.
const ADDER = 'erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug';
async function main(): Promise<void> {
const provider = new DevnetEntrypoint({ clientName: 'mvx-cookbook' }).createNetworkProvider();
const econ = await getEconomics(provider);
console.log('GET economics (no typed provider method exists):');
console.log(` price $${econ.price}, marketCap ${econ.marketCap}, staked ${econ.staked}`);
const stats = await getStats(provider);
console.log('GET stats:');
console.log(` shards ${stats.shards}, accounts ${stats.accounts}, transactions ${stats.transactions}`);
const sum = await rawQuery(provider, ADDER, 'getSum');
console.log('POST query — adder.getSum() at the raw HTTP level:');
console.log(` decoded returnData: ${sum}`);
}
main().catch((err: unknown) => {
console.error(err);
process.exitCode = 1;
});
Run it
npm start
Expected output (economics and the adder sum are live, so they move):
GET economics (no typed provider method exists):
price $3.26, marketCap 84056590, staked 2406394
GET stats:
shards 3, accounts 18, transactions 25902940
POST query — adder.getSum() at the raw HTTP level:
decoded returnData: 84
How it works
economics is the flagship raw GET. Total supply, price, market cap, APR,
there is no typed provider method for it, so doGetGeneric('economics') is the
only way. stats is another. This is exactly what the escape hatch is for:
endpoints that exist on the network but not (yet) in the SDK's typed surface.
doPostGeneric('query', ...) is SmartContractController.query at the HTTP
level. The raw VM query returns returnData as base64-encoded, big-endian
bytes; the recipe decodes the first value to a bigint, adder's getSum(), the
same 84 the typed query-contract-view recipe returns. Use the typed controller
in real code; this shows the raw layer underneath, and the shape you would reuse
for any unwrapped POST endpoint.
Query params go in the path string. doGetGeneric takes the whole resource
path, so pagination and filters ride along as a query string, e.g.
doGetGeneric('accounts/erd1.../tokens?from=0&size=10'). There is no separate
params argument.
Pitfalls
doGetGeneric / doPostGeneric return any. Casting to an interface (as this
recipe does) gives your code real types, but nothing validates the cast at
runtime. Treat these responses like any other untrusted input, a renamed field
will not be a compile error, it will be undefined at runtime.
doGetGeneric prepends the provider's base URL, so pass economics, not
/economics or a full URL. And the Api and Proxy expose different paths,
economics and stats are API routes; a gateway (Proxy) has its own
network/... and address/... routes. The escape hatch does not paper over that
difference.
If a typed method covers your need, use it, you get decoding, types, and stability
across SDK versions for free. Reach for doGetGeneric / doPostGeneric only for
endpoints the SDK does not model yet, and consider filing an issue so it gets a
typed method.
See also
- Configure a network provider
is the provider whose
doGetGeneric/doPostGenericthis recipe uses. - Fetch an account's token balances has typed methods for the token endpoints, so you rarely need the raw layer there.
- Fetch token metadata is another place a typed method saves you from a raw request.