Skip to main content

HTTP Functions

HTTP functions create custom HTTP endpoints that execute your code on demand. Use them for custom APIs, webhook handlers, and complex operations.

How It Works

  1. Define a unique endpoint path (e.g., calculate-shipping)
  2. Configure which HTTP methods are allowed (GET, POST)
  3. Set role-based access control per method
  4. Write JavaScript code to handle requests
  5. Call the endpoint via HTTP

URL Structure

/api/{function-name}

Example:

/api/calculate-shipping
External Clients

External clients should replace /api with https://<region>.restapi.com/<api-name>. See Hosted Webapps for details.

Usage:

fetch("/api/calculate-shipping", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ weight: 2.5 }),
});
warning

The path is /api/<function-name> — not /<function-name>. The /api prefix is required.

Configuration

Basic Settings

SettingDescription
NameUnique identifier for the function (used in URL)
DescriptionOptional documentation
EnabledToggle to enable/disable

HTTP Methods

Configure which methods the endpoint accepts:

MethodTypical Use
GETRetrieve data, calculations
POSTCreate resources, complex operations

Access Control

Unlike trigger and timer functions, HTTP functions have role-based access control per HTTP method:

RoleDescription
Anonymous userNo authentication required
Authenticated userAny logged-in user
Custom rolesYour defined roles

Configure different permissions for each method. For example:

  • GET → Anonymous (public)
  • POST → Authenticated users only

Execution Context

What Your Code Receives

ObjectDescription
req.bodyJsonParsed JSON request body
req.bodyRaw request body string
req.methodHTTP method (GET, POST)
req.headersRequest headers (filtered for security)
req.queryQuery parameters
meAuthenticated user information (if any)
apiAPI information
secretsConfigured secrets

Returning Responses

Set the res object to return data:

// Return JSON response
res = {
status: 200,
bodyJson: {
success: true,
result: calculatedValue,
},
};

// Or just set bodyJson (status defaults to 200)
res = {
bodyJson: { message: "Done" },
};
Writing as a default export? Don't mix forms.

The examples above use the script form (bare global res, no export default). You cannot mix this with the default-export form — pick one per file.

If your file has export default (ctx) => {...}, set the response via ctx.res. A bare global res = {...} is silently discarded the moment an export default exists — the response is lost and the function appears to do nothing. See Two Function Forms.

Typed Parameters

/api/_openapi and the generated types.d.ts describe what a function accepts and returns, so typed clients (typescript-fetch, openapi-generator) — and code-generating LLMs — call it with the right shapes and get a typed result.

A function gets described in one of two ways, and they compose:

  1. Read from your code, automatically, with no configuration.
  2. Declared on the function, explicitly. A declared side always wins, because the runtime enforces declared inputs.

Which functions get types from their code

All of these must hold. Miss one and that side stays an opaque object — nothing breaks, you simply get no description.

RequirementWhy
A file-based functionCode pasted inline into the function has no file to read.
The default-export formScript form has no signature: no ctx to inspect, no return type to read. See Two Function Forms.
A .ts, .tsx, .js, .mjs or .cjs fileDeclaration files (.d.ts) and tests (*.test.ts, *_test.ts) are skipped.
Types that survive conversion to JSON SchemaSee all or nothing below.

TypeScript gives the best result, but JavaScript works too — types are inferred from what you actually return. In JavaScript the ctx parameter is untyped, so usually only the response side can be read.

Where each side comes from

First match wins:

SideRead from
Request bodyctx.req.bodyJson — the TBody in FunctionContext<TItem, TBody, TRes> — then export type Input
Response bodyctx.res.bodyJson (TRes), then the function's return type, then export type Output
Queryctx.req.query, but only once you narrow it past the platform's Record<string, string>

For most functions the return type is all it takes:

import type { FunctionContext } from '../types';

interface Receipt {
/** What it cost, in minor units. */
cost: number;
status: 'ok' | 'rejected';
}

export default async function (ctx: FunctionContext): Promise<Receipt> {
return { cost: 1250, status: 'ok' };
}

That publishes cost as a number carrying its JSDoc as the description, and status as an enum of "ok" and "rejected" — with nothing declared anywhere.

Writing ctx.res yourself? Then type it.

If your code assigns the response — ctx.res = {...} or ctx.res.bodyJson = ... — the return type is ignored. At runtime the returned value is not what the caller receives, so publishing it would describe the wrong shape with confidence. Type the response through FunctionContext<unknown, unknown, Receipt> or export type Output instead.

All or nothing, per side

A side is published only when every part of it converts to JSON Schema. One property the converter can't express — an any, a function, a tuple — drops that entire side rather than just that property. A schema missing a field is a wrong description of your endpoint; an opaque object is an honest one.

This is also the usual reason a function you did type publishes nothing: an import that can't be resolved types as any and takes the rest of the shape down with it.

Advisory, never enforced

Types read from your code only describe the endpoint. Nothing is validated against them, and no request is ever rejected because of them. Declared queryParams and requestBody are the opposite — see the table below.

Declaring types explicitly

Declare the query parameters, request-body fields, and response fields a function works with when you want them enforced, or when the code can't be read (script form, inline code, a shape that won't convert). A declared side replaces whatever the code said.

Set them on the function via the API, the CLI, or a schema import:

{
"name": "calculate-shipping",
"functionTypeId": 2,
"endpoint": "/calculate-shipping",
"queryParams": [
{ "name": "currency", "type": "string", "spec": { "enum": ["USD", "EUR", "NOK"] } }
],
"requestBody": [
{ "name": "weight", "type": "decimal", "isRequired": true },
{ "name": "destination", "type": "string", "isRequired": true }
],
"responseFields": [
{ "name": "cost", "type": "decimal" },
{ "name": "estimatedDays", "type": "integer" },
{
"name": "breakdown",
"type": "object",
"spec": { "properties": { "base": { "type": "number" }, "fuel": { "type": "number" } } }
}
]
}

Each entry is a typed field: a name, a type, an optional isRequired, and an optional field spec (enum, description, a nested object shape, …).

Input is enforced, output is advisory

The three sides are not symmetric:

SideTypesAt runtime
queryParams, requestBodyscalar only¹Enforced — a missing required or wrong-typed value is rejected with 400 before your code runs.
responseFieldsany, including objectAdvisory — never validated; it only types the response for clients.

¹ string, integer, decimal, guid, boolean, date, date-time, text. Objects and blobs can only be response fields — the runtime validator can't check them, so they're rejected as input. A typical function is therefore scalar in, rich object out.

Adding a required input is a breaking change

Because inputs are enforced, adding a required queryParams or requestBody field makes every existing caller that omits it start receiving 400s. Declare required inputs when you first create the function; add later ones as optional.

Regenerate your client

Both declared parameters and types read from your code flow into /api/_openapi and types.d.ts — regenerate your client (e.g. gg rat api -a <api> types) to pick them up. Types read from code are re-read when the API's functions change, so deploying a new version of a function is enough to update the document. The function becomes a typed method whose body and return type match your declaration:

// types.d.ts (generated)
post(path: `/calculate-shipping${string}`, body: { weight: number; destination: string }):
Promise<{ cost?: number; estimatedDays?: number; breakdown?: { base?: number; fuel?: number } }>;
No portal editor yet

Declared parameters are set through the API, CLI, or schema import — the developer portal doesn't have an editor for them yet. Functions that declare nothing keep working exactly as before: they are described by their code where that can be read, and as an opaque object where it can't.

Use Cases

Custom Calculations

Perform calculations that require server-side logic:

const { weight, destination, method } = req.bodyJson;

let baseCost = weight * 0.5;

if (destination === "international") {
baseCost *= 2.5;
}

if (method === "express") {
baseCost *= 1.5;
}

res = {
bodyJson: {
shippingCost: Math.round(baseCost * 100) / 100,
estimatedDays: method === "express" ? 2 : 7,
},
};

Webhook Handlers

Receive webhooks from external services:

// Handle Stripe webhook
const event = req.bodyJson;

switch (event.type) {
case "payment_intent.succeeded":
const paymentIntent = event.data.object;

// Update order status using built-in method
await patch(`/orders/${paymentIntent.metadata.orderId}`, {
paymentStatus: "paid",
stripePaymentId: paymentIntent.id,
});
break;

case "payment_intent.payment_failed":
// Handle failure
break;
}

res = { bodyJson: { received: true } };

Aggregation Endpoints

Create endpoints that aggregate data:

// Dashboard statistics - use built-in methods
const [ordersRes, customersRes, productsRes] = await Promise.all([
get("/orders?count=true"),
get("/customers?count=true"),
get("/products?count=true"),
]);

res = {
bodyJson: {
totalOrders: ordersRes.meta?.count || 0,
totalCustomers: customersRes.meta?.count || 0,
totalProducts: productsRes.meta?.count || 0,
recentOrders: ordersRes.data.slice(0, 5),
},
};

Multi-Step Operations

Orchestrate complex operations:

// Create order with inventory check
const { customerId, items } = req.bodyJson;

// Check inventory using built-in method
for (const item of items) {
const productRes = await get(`/products/${item.productId}`);
const product = productRes.data[0];

if (product.stockLevel < item.quantity) {
res = {
status: 400,
bodyJson: {
error: `Insufficient stock for ${product.name}`,
available: product.stockLevel,
},
};
return;
}
}

// Create order
const orderRes = await post("/orders", {
customerId,
items,
status: "pending",
total: items.reduce((sum, i) => sum + i.price * i.quantity, 0),
});

const orderId = orderRes.data[0];

// Reserve inventory
for (const item of items) {
const productRes = await get(`/products/${item.productId}`);
const product = productRes.data[0];

await patch(`/products/${item.productId}`, {
stockLevel: product.stockLevel - item.quantity,
});
}

res = { bodyJson: { orderId, status: "created" } };

Validation with Errors

Use validationErrors for user-facing validation:

const { email, name, age } = req.bodyJson;

if (!email) {
validationErrors.push("Email is required");
}

if (!name || name.length < 2) {
validationErrors.push("Name must be at least 2 characters");
}

if (age && age < 18) {
validationErrors.push("Must be 18 or older");
}

// If validationErrors has items, returns 400 automatically
if (validationErrors.length > 0) {
return; // Stop execution
}

// Proceed with valid data
const userRes = await post("/users", { email, name, age });

res = { bodyJson: { userId: userRes.data[0], created: true } };

External API Integration

Call external APIs (requires paid tier):

// Verify address with external service
const { address } = req.bodyJson;

const verification = await fetch("https://api.addressvalidator.com/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${secrets.ADDRESS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ address }),
});

const result = await verification.json();

res = {
bodyJson: {
valid: result.valid,
standardized: result.standardizedAddress,
},
};

Calling HTTP Functions

Never from another function

HTTP functions are entry points for external clients — web apps, integrations, cURL. Never call one from another function: function-to-function calls can loop forever. Import a shared module instead.

From a Hosted Web App

If your frontend is hosted on RestAPI.com, use the /api virtual path — no CORS or auth headers needed:

const response = await fetch("/api/calculate-shipping", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
weight: 2.5,
destination: "domestic",
method: "standard",
}),
});

const result = await response.json();
console.log(result.shippingCost);

From External Clients

Use the full region-specific URL with an auth token:

const response = await fetch(
"https://<region>.restapi.com/<api-name>/calculate-shipping",
{
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
},
body: JSON.stringify({
weight: 2.5,
destination: "domestic",
method: "standard",
}),
},
);

const result = await response.json();
console.log(result.shippingCost);

From cURL

curl -X POST https://<region>.restapi.com/<api-name>/calculate-shipping \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"weight": 2.5, "destination": "domestic", "method": "standard"}'

Best Practices

  • Use meaningful function namescalculate-shipping is better than func1
  • Validate input — Check req.bodyJson fields and use validationErrors
  • Use built-in methodsget(), post(), patch() for reading and writing collections
  • Use fetch only for external APIs — External calls require paid tier
  • Set appropriate access control — Don't expose sensitive operations publicly
  • Return consistent responses — Use a standard response structure