# Edge Function returns 401

> Fix 401 Unauthorized from a SupaHost Edge Function: UNAUTHORIZED_NO_AUTH_HEADER and UNAUTHORIZED_INVALID_JWT, the Require JWT toggle, webhooks and calls from cron or pg_net.

Source: https://supahost.app/kb/function-401 · Updated 2026-11-11

## What you see

Calling a function returns `401` with a JSON body and an `sb-error-code` response header set to one of two codes:

```text
HTTP/2 401
sb-error-code: UNAUTHORIZED_NO_AUTH_HEADER
```

```text
HTTP/2 401
sb-error-code: UNAUTHORIZED_INVALID_JWT
```

In supabase-js, `supabase.functions.invoke()` returns a `FunctionsHttpError` with `context.status` 401. In the dashboard's Logs card the request line shows status 401 and your function's own logs are missing, because your code never ran.

## Why it happens

When **Require JWT** is on for a function (the default), SupaHost's function router checks the `Authorization: Bearer <token>` header before your code runs:

- `UNAUTHORIZED_NO_AUTH_HEADER`: there's no `Authorization` header. Typical with plain `fetch`, curl, webhooks from Stripe or GitHub, and cron jobs that call the function over HTTP.
- `UNAUTHORIZED_INVALID_JWT`: the token isn't a valid JWT for this project. It's signed with another project's secret (each project has its own keys), it has expired (`exp`), it isn't valid yet (`nbf`), or it's not a JWT at all.

If the 401 has **no** `sb-error-code` header, it came from your own code, for example a `getUser()` check that failed.

## How to fix it

1. **Call through supabase-js.** `invoke` sends the signed-in user's access token, or the anon key when nobody is signed in:

   ```js
   const { data, error } = await supabase.functions.invoke('send-invoice', {
     body: { invoiceId: 1042 },
   })
   ```

2. **Add the header for manual calls:**

   ```bash
   curl -X POST 'https://app-one.acme.supahost.space/functions/v1/send-invoice' \
     -H "Authorization: Bearer $SUPABASE_ANON_KEY" \
     -H 'Content-Type: application/json' \
     -d '{"invoiceId":1042}'
   ```

3. **Turn off Require JWT for webhooks.** Services like Stripe can't send your JWT. In *Dashboard → your project → **Functions** tab*, switch off the function's **Require JWT** toggle, then verify the provider's signature inside the function instead. See [Example: Stripe webhooks](/docs/functions/stripe-webhooks).
4. **Use the right project's key.** After copying code from another project, or importing from Supabase Cloud, replace the old keys and have users sign in again so their tokens are issued by this project.
5. **Refresh expired user tokens.** A session token that sat in local storage past its lifetime fails. supabase-js refreshes it automatically if the client is alive; for server code, get a fresh session before calling.
6. **For pg_net or cron calls,** include the header. Store the key in Vault and read it in the job, rather than pasting it into SQL. See [Running functions on a schedule](/docs/functions/scheduling).

## On SupaHost

The router accepts **any** valid JWT for the project, including the public anon key. So Require JWT stops random internet traffic, but it doesn't prove who the caller is. If a function acts on a user's data, check the user inside the function:

```ts
import { createClient } from 'npm:@supabase/supabase-js@2'

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_ANON_KEY')!,
)

Deno.serve(async (req) => {
  const token = (req.headers.get('Authorization') ?? '').replace('Bearer ', '')
  const { data: { user } } = await supabase.auth.getUser(token)
  if (!user) return Response.json({ error: 'Sign in first' }, { status: 401 })
  // ...act on behalf of user.id
  return Response.json({ ok: true })
})
```

`OPTIONS` preflight requests skip the JWT check, so CORS preflights don't fail with 401. If you send only an `apikey` header without `Authorization`, SupaHost's gateway copies it into `Authorization: Bearer`, so that works too.

## Related

- [Authentication and JWT verification](/docs/functions/auth)
- [Calling functions from your app](/docs/functions/calling-functions)
- [CORS error calling an Edge Function](/kb/function-cors-error)
- [JWT expired](/kb/jwt-expired)
