Browse topics
On this page
HTTP API
Outbound HTTP from applet methods: plain requests, binary and streaming bodies, resource-scoped authed requests, and pagination walkers.
ctx.sdk.http makes outbound HTTP requests from inside an applet method, routed through the native host so you are not bound by browser CORS rules. It covers plain requests (text, binary, streaming), resource-scoped authenticated requests that inject the auth header for you, and a paginated walker that follows cursor, link-header, or page-number schemes.
Scope required: http:fetch for request and stream. network:http for requestAuthed and requestPaginated.
Outbound hosts must match the calling applet’s outboundUrl[] allowlist in applet.json (["*"] to allow any).
Usage
Inside a method run, reach the surface via ctx.sdk.http.
import { defineMethod } from "@rightplace/applet-sdk/v2";
export const fetchFeed = defineMethod({
name: "@wiredwp/reader.fetchFeed",
async run({ url }, ctx) {
const res = await ctx.sdk.http.request({
url,
method: "GET",
headers: { "User-Agent": "RightPlace/1.0" },
timeout: 30_000,
signal: ctx.signal, // propagate cancellation
});
// res = { status, headers, body: string }
return res.body;
},
});
The default User-Agent is RightPlace/1.0 unless you set your own header. timeout is in milliseconds. Pass ctx.signal to let host-side cancellation abort the request.
Binary responses
Pass responseType: "binary" to receive raw bytes for image downloads, audio, archive blobs, and similar. The body field comes back as a Uint8Array instead of a string.
const res = await ctx.sdk.http.request({
url: "https://example.com/favicon.png",
method: "GET",
responseType: "binary",
signal: ctx.signal,
});
// res = { status, headers, body: Uint8Array }
await ctx.sdk.fs.writeFileBytes(
`${ctx.resourceFolderPath}/favicons/feed-${feedId}.png`,
res.body
);
The default (no responseType, or responseType: "text") returns a UTF-8 string.
Streaming
For Server-Sent Events, LLM token streams, or large bodies, iterate over stream. Each chunk is a Uint8Array.
for await (const chunk of ctx.sdk.http.stream({ url, method: "GET" })) {
process(chunk); // Uint8Array chunks
}
Authenticated requests
requestAuthed resolves the owner applet’s httpProfile for the resource’s type, interpolates the URL template against the resource’s configJson, and injects the auth header from the resource credential. The secret never crosses into the bundle, so you never handle the token directly.
Declare the profile once per owner applet in applet.json:
// applet.json
"httpProfile": {
"baseUrlTemplate": "https://{config.storeUrl}/admin/api/2025-01/",
"auth": {
"type": "header",
"name": "X-Shopify-Access-Token",
"valuePrefix": "", // optional, e.g. "Bearer "
"credential": {
"source": "websiteCredential",
"key": "{config.credentialKey}"
}
}
}
Then call it with a resourceId and a path that joins onto baseUrlTemplate:
const res = await ctx.sdk.http.requestAuthed({
resourceId,
method: "POST", // "GET" | "POST" | "PUT" | "DELETE"
path: "orders/123/cancel.json", // joined with baseUrlTemplate
body: JSON.stringify({ reason: "customer" }),
headers: { "Content-Type": "application/json" },
});
// res = { status, statusText, ok, headers, body: string }
Sub-applets inherit the owner’s profile through parentApplet. The credential itself is stored and resolved through the host, see credentials.
Pagination
requestPaginated builds on requestAuthed. The host runs the page loop, follows the declared pagination shape, substitutes cursor or page state per page, applies a max-pages cap, and returns the flattened items. You describe how to walk via the pagination block instead of writing the loop yourself.
GraphQL relay-cursor walk:
const { items, pagesFetched, truncated } = await ctx.sdk.http.requestPaginated({
resourceId,
method: "POST",
path: "graphql.json",
headers: { "Content-Type": "application/json" },
bodyTemplate: {
query: `query($first: Int!, $after: String) {
taxonomy { categories(first: $first, after: $after) {
edges { node { id fullName } cursor }
pageInfo { hasNextPage }
}}
}`,
variables: {}, // host injects { first, after } per page
},
pagination: {
style: "graphql-cursor",
itemsPath: "data.taxonomy.categories.edges",
nextCursorPath: "data.taxonomy.categories.edges[-1].cursor",
hasNextPath: "data.taxonomy.categories.pageInfo.hasNextPage",
cursorArgName: "after", // goes to body.variables.after
pageSize: 250, // goes to body.variables.first
},
});
// items: every edge from every page, flattened. Caller does the transform.
REST Link: rel="next" walk:
const { items } = await ctx.sdk.http.requestPaginated({
resourceId, method: "GET", path: "orders.json?limit=250&status=any",
pagination: { style: "rest-link-header", itemsPath: "orders" },
});
REST ?page=N&per_page=M walk:
const { items } = await ctx.sdk.http.requestPaginated({
resourceId, method: "GET", path: "wp-json/wp/v2/posts",
pagination: {
style: "rest-page-number",
itemsPath: "", // body itself is the array
pageParam: "page",
pageSizeParam: "per_page",
pageSize: 100,
},
});
Dotted JSON path syntax used by itemsPath, nextCursorPath, and hasNextPath:
data.x.y.zwalks object fields.data.x.edges[0]selects the first array element.data.x.edges[-1]selects the last array element (where a GraphQL cursor usually lives).
Notes
requestandstreamneedhttp:fetch.requestAuthedandrequestPaginatedneednetwork:http. Declare the scope inapplet.json::scopes[].- Every URL must match the calling applet’s
outboundUrl[]allowlist, checked per page for paginated walks. requestPaginateddefault max-pages cap is 500. Override withpagination.maxPages. Empty pages and missing cursors terminate the walk so a broken cursor cannot loop forever.- Do not add per-vendor
cmd_<vendor>_<verb>Rust commands for REST or GraphQL endpoints. Compose them overrequestAuthedandrequestPaginated.