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
- Define a unique endpoint path (e.g.,
calculate-shipping) - Configure which HTTP methods are allowed (GET, POST)
- Set role-based access control per method
- Write JavaScript code to handle requests
- Call the endpoint via HTTP
URL Structure
/api/{function-name}
Example:
/api/calculate-shipping
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 }),
});
The path is /api/<function-name> — not /<function-name>. The /api prefix is required.
Configuration
Basic Settings
| Setting | Description |
|---|---|
| Name | Unique identifier for the function (used in URL) |
| Description | Optional documentation |
| Enabled | Toggle to enable/disable |
HTTP Methods
Configure which methods the endpoint accepts:
| Method | Typical Use |
|---|---|
GET | Retrieve data, calculations |
POST | Create resources, complex operations |
Access Control
Unlike trigger and timer functions, HTTP functions have role-based access control per HTTP method:
| Role | Description |
|---|---|
| Anonymous user | No authentication required |
| Authenticated user | Any logged-in user |
| Custom roles | Your defined roles |
Configure different permissions for each method. For example:
GET→ Anonymous (public)POST→ Authenticated users only
Execution Context
What Your Code Receives
| Object | Description |
|---|---|
req.bodyJson | Parsed JSON request body |
req.body | Raw request body string |
req.method | HTTP method (GET, POST) |
req.headers | Request headers (filtered for security) |
req.query | Query parameters |
me | Authenticated user information (if any) |
api | API information |
secrets | Configured 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" },
};
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:
- Read from your code, automatically, with no configuration.
- 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.
| Requirement | Why |
|---|---|
| A file-based function | Code pasted inline into the function has no file to read. |
| The default-export form | Script form has no signature: no ctx to inspect, no return type to read. See Two Function Forms. |
A .ts, .tsx, .js, .mjs or .cjs file | Declaration files (.d.ts) and tests (*.test.ts, *_test.ts) are skipped. |
| Types that survive conversion to JSON Schema | See 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:
| Side | Read from |
|---|---|
| Request body | ctx.req.bodyJson — the TBody in FunctionContext<TItem, TBody, TRes> — then export type Input |
| Response body | ctx.res.bodyJson (TRes), then the function's return type, then export type Output |
| Query | ctx.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.
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.
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.
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:
| Side | Types | At runtime |
|---|---|---|
queryParams, requestBody | scalar only¹ | Enforced — a missing required or wrong-typed value is rejected with 400 before your code runs. |
responseFields | any, including object | Advisory — 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.
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 } }>;
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
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 names —
calculate-shippingis better thanfunc1 - Validate input — Check
req.bodyJsonfields and usevalidationErrors - Use built-in methods —
get(),post(),patch()for reading and writing collections - Use
fetchonly 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