Calling functions from your app
Call SupaHost Edge Functions with supabase-js functions.invoke, fetch, curl and other clients, pass bodies and headers, call sub-paths and handle errors.
A deployed function is an HTTPS endpoint, so anything that can send a request can call it. supabase-js wraps that in functions.invoke, which adds the right headers for you.
Every function URL has the form:
https://app-one.acme.supahost.space/functions/v1/<function-name>
With supabase-js
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);
const { data, error } = await supabase.functions.invoke("create-invoice", {
body: { customerId: "c_381", lines: [{ item: "Design work", hours: 6, rate: 90 }] },
});
invoke sends:
Authorization: Bearer <token>: the signed-in user's access token if there's a session, otherwise the anon key. That's what the Require a JWT check looks at.apikeywith the anon key. SupaHost doesn't need it on/functions/v1/*, but it does no harm.- A
Content-Typethat matches the body.
Bodies, methods and headers
You pass as body |
Sent as |
|---|---|
| A plain object or array | JSON (application/json) |
| A string | text/plain |
FormData |
multipart/form-data |
Blob / File / ArrayBuffer |
application/octet-stream |
The default method is POST. Change it, and add your own headers, with the options object:
const { data, error } = await supabase.functions.invoke("invoices/inv_1042", {
method: "GET",
headers: { "x-request-source": "dashboard" },
});
Putting a path after the name (invoices/inv_1042) calls the same function with a longer path, which is how you build small REST APIs inside one function (see Writing functions). Add any custom header to your function's CORS Access-Control-Allow-Headers list.
data is parsed from the response's Content-Type: an object for application/json, a Blob for application/octet-stream, FormData for multipart/form-data, the raw Response for text/event-stream, and text for anything else.
Handling errors
error is one of three classes exported from supabase-js:
| Class | When |
|---|---|
FunctionsHttpError |
The function (or the router) answered with a non-2xx status. error.context is the Response. |
FunctionsRelayError |
The request failed between the gateway and the function. |
FunctionsFetchError |
The request never got a response (network down, DNS, CORS blocked). |
import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from "@supabase/supabase-js";
const { data, error } = await supabase.functions.invoke("create-invoice", { body: draft });
if (error instanceof FunctionsHttpError) {
const code = error.context.headers.get("sb-error-code"); // set on router errors, e.g. UNAUTHORIZED_INVALID_JWT
const details = await error.context.json().catch(() => null);
console.error(`Function failed (${error.context.status})`, code, details);
} else if (error instanceof FunctionsRelayError) {
console.error("Relay error:", error.message);
} else if (error instanceof FunctionsFetchError) {
console.error("Network or CORS problem:", error.message);
} else {
console.log("Invoice created", data);
}
The sb-error-code header tells router errors (NOT_FOUND, UNAUTHORIZED_NO_AUTH_HEADER, UNAUTHORIZED_INVALID_JWT, BOOT_ERROR, WORKER_RESOURCE_LIMIT, IDLE_TIMEOUT, WORKER_ERROR) apart from errors your own code returned. See Logs and debugging for what each means.
With fetch
Use fetch when you want a streamed response or don't use supabase-js:
const { data: { session } } = await supabase.auth.getSession();
const res = await fetch(`${SUPABASE_URL}/functions/v1/export-invoices`, {
method: "POST",
headers: {
Authorization: `Bearer ${session?.access_token ?? SUPABASE_ANON_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ month: "2026-10" }),
});
if (!res.ok) throw new Error(`export-invoices failed: ${res.status}`);
const csv = await res.text();
From a terminal
curl -i -X POST "https://app-one.acme.supahost.space/functions/v1/create-invoice" \
-H "Authorization: Bearer $SUPABASE_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"customerId":"c_381","lines":[{"item":"Audit","hours":2,"rate":120}]}'
-i prints the response headers, including sb-error-code when the router rejects the call.
From your server
Server code can call a function as a user (forward their access token) or as your backend. For backend-only functions, send the service_role key and check the role inside the function (see Authentication and JWT verification):
import { createClient } from "@supabase/supabase-js";
// Server only: never ship the service_role key to a browser.
const admin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, {
auth: { persistSession: false },
});
const { data, error } = await admin.functions.invoke("rebuild-reports", { body: { since: "2026-11-01" } });
From another function
Inside a function, create a client with SUPABASE_URL and the key you want to call with, then use functions.invoke the same way. Chained calls each count toward the 150-second response limit of the outer function, so keep them short.
From the database
To call a function when a row changes, use a database webhook or net.http_post from a trigger: see Database webhooks and HTTP requests with pg_net. For a timer, see Running functions on a schedule.
Other client libraries
The Flutter, Swift, Kotlin and Python Supabase clients all have a functions.invoke method that works the same way against your SupaHost project URL. See the JavaScript reference for functions.invoke for every option.