# How to Write HTTPS Callable Functions in Firebase

- Tool: Firebase
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Firebase Cloud Functions v2, Firebase JS SDK v9+, Node.js 18+, Blaze plan required
- Last updated: March 2026

## TL;DR

Firebase HTTPS callable functions use the onCall handler from firebase-functions/v2/https to create server-side endpoints that automatically handle authentication, CORS, and data serialization. The client calls them with httpsCallable from the Firebase SDK. Unlike raw onRequest functions, callable functions verify the Firebase ID token, pass the authenticated user's context to your handler, and serialize request/response data automatically.

## Creating Secure Server-Side Endpoints with Firebase Callable Functions

Callable functions are the recommended way to run server-side logic from Firebase client apps. They handle auth token verification, CORS headers, and JSON serialization out of the box, so you can focus on business logic. This tutorial shows you how to write a v2 callable function, call it from a web app, validate inputs, handle errors, and test locally with emulators.

## Before you start

- A Firebase project on the Blaze plan (required for Cloud Functions)
- Cloud Functions initialized in your project (firebase init functions)
- Firebase JS SDK v9+ installed in your client app
- Node.js 18+ installed

## Step-by-step guide

### 1. Create a basic callable function with onCall

Import onCall from firebase-functions/v2/https and export a named function. The handler receives a request object containing data (the payload from the client) and auth (the authenticated user's token, if any). Return a value or object that Firebase automatically serializes and sends to the client.

```
// functions/src/index.ts
import { onCall, HttpsError } from 'firebase-functions/v2/https'

export const greetUser = onCall((request) => {
  // request.auth is populated if the caller is authenticated
  if (!request.auth) {
    throw new HttpsError('unauthenticated', 'You must be signed in')
  }

  const name = request.data.name
  if (!name || typeof name !== 'string') {
    throw new HttpsError('invalid-argument', 'Name is required')
  }

  return {
    message: `Hello, ${name}! Your UID is ${request.auth.uid}`
  }
})
```

**Expected result:** The function is ready to deploy and will greet authenticated users by name.

### 2. Call the function from the client

Import getFunctions and httpsCallable from firebase/functions. Create a callable reference using the function name, then call it with your data payload. The response contains a data property with whatever the function returned.

```
// src/lib/functions.ts
import { getFunctions, httpsCallable } from 'firebase/functions'

const functions = getFunctions()

export async function greetUser(name: string) {
  const greet = httpsCallable(functions, 'greetUser')
  const result = await greet({ name })
  return result.data as { message: string }
}

// Usage in a component
const response = await greetUser('Alice')
console.log(response.message) // "Hello, Alice! Your UID is abc123"
```

**Expected result:** The client receives the response object from the callable function with the greeting message.

### 3. Add input validation with HttpsError

Use HttpsError to throw structured errors that the client can handle by code. Firebase defines standard error codes: 'invalid-argument', 'unauthenticated', 'permission-denied', 'not-found', 'already-exists', and more. The client receives these as a FirebaseError with the code and message.

```
// functions/src/index.ts
import { onCall, HttpsError } from 'firebase-functions/v2/https'

interface CreatePostData {
  title: string
  body: string
  category: string
}

export const createPost = onCall(async (request) => {
  if (!request.auth) {
    throw new HttpsError('unauthenticated', 'Sign in required')
  }

  const { title, body, category } = request.data as CreatePostData

  if (!title || title.length > 200) {
    throw new HttpsError('invalid-argument', 'Title must be 1-200 characters')
  }
  if (!body || body.length > 10000) {
    throw new HttpsError('invalid-argument', 'Body must be 1-10000 characters')
  }
  if (!['tech', 'design', 'business'].includes(category)) {
    throw new HttpsError('invalid-argument', 'Invalid category')
  }

  // Use Admin SDK to write to Firestore (bypasses security rules)
  const { getFirestore } = await import('firebase-admin/firestore')
  const db = getFirestore()

  const docRef = await db.collection('posts').add({
    title,
    body,
    category,
    authorId: request.auth.uid,
    createdAt: new Date()
  })

  return { postId: docRef.id }
})
```

**Expected result:** Invalid input throws a typed error the client can catch. Valid input creates a Firestore document and returns the new ID.

### 4. Handle errors on the client

When a callable function throws an HttpsError, the client receives it as a standard error. Catch it and check the code property to display appropriate messages. The error includes the code, message, and optional details.

```
import { getFunctions, httpsCallable } from 'firebase/functions'
import { FirebaseError } from 'firebase/app'

const functions = getFunctions()

async function submitPost(title: string, body: string, category: string) {
  const createPost = httpsCallable(functions, 'createPost')

  try {
    const result = await createPost({ title, body, category })
    const { postId } = result.data as { postId: string }
    console.log('Post created:', postId)
    return postId
  } catch (error) {
    if (error instanceof FirebaseError) {
      switch (error.code) {
        case 'functions/unauthenticated':
          alert('Please sign in to create a post')
          break
        case 'functions/invalid-argument':
          alert(error.message)
          break
        default:
          alert('Something went wrong. Please try again.')
      }
    }
    throw error
  }
}
```

**Expected result:** Specific error types trigger targeted error messages in the UI.

### 5. Test the callable function with the Emulator Suite

Connect your client to the Functions emulator during development so you can test without deploying. The emulator runs your function code locally and provides instant feedback through logs in the terminal.

```
// In your client initialization code
import { getFunctions, connectFunctionsEmulator } from 'firebase/functions'

const functions = getFunctions()

if (import.meta.env.DEV) {
  connectFunctionsEmulator(functions, '127.0.0.1', 5001)
}

// Start the emulator:
// firebase emulators:start --only functions
```

**Expected result:** Client calls route to the local Functions emulator. Function logs appear in the terminal running the emulators.

### 6. Deploy the callable function

Deploy your function to Firebase and verify it works in production. Use selective deployment to push only functions. After deployment, remove the emulator connection on the client and the function is live.

```
firebase deploy --only functions:greetUser,functions:createPost
```

**Expected result:** Functions are deployed and accessible from your production client app.

## Complete code example

File: `functions/src/index.ts`

```typescript
// functions/src/index.ts
import { onCall, HttpsError } from 'firebase-functions/v2/https'
import { initializeApp } from 'firebase-admin/app'
import { getFirestore } from 'firebase-admin/firestore'

initializeApp()
const db = getFirestore()

// Simple callable function with auth check
export const greetUser = onCall((request) => {
  if (!request.auth) {
    throw new HttpsError('unauthenticated', 'You must be signed in')
  }
  const name = request.data.name
  if (!name || typeof name !== 'string') {
    throw new HttpsError('invalid-argument', 'Name is required')
  }
  return { message: `Hello, ${name}!`, uid: request.auth.uid }
})

// Callable function with Firestore write
export const createPost = onCall(async (request) => {
  if (!request.auth) {
    throw new HttpsError('unauthenticated', 'Sign in required')
  }

  const { title, body, category } = request.data
  if (!title || typeof title !== 'string' || title.length > 200) {
    throw new HttpsError('invalid-argument', 'Title must be 1-200 chars')
  }
  if (!body || typeof body !== 'string' || body.length > 10000) {
    throw new HttpsError('invalid-argument', 'Body must be 1-10000 chars')
  }

  const docRef = await db.collection('posts').add({
    title,
    body,
    category: category || 'general',
    authorId: request.auth.uid,
    authorEmail: request.auth.token.email || null,
    createdAt: new Date(),
    updatedAt: new Date()
  })

  return { postId: docRef.id }
})

// Callable function to delete own post
export const deletePost = onCall(async (request) => {
  if (!request.auth) {
    throw new HttpsError('unauthenticated', 'Sign in required')
  }

  const { postId } = request.data
  if (!postId || typeof postId !== 'string') {
    throw new HttpsError('invalid-argument', 'Post ID is required')
  }

  const postRef = db.collection('posts').doc(postId)
  const post = await postRef.get()

  if (!post.exists) {
    throw new HttpsError('not-found', 'Post does not exist')
  }
  if (post.data()?.authorId !== request.auth.uid) {
    throw new HttpsError('permission-denied', 'You can only delete your own posts')
  }

  await postRef.delete()
  return { deleted: true }
})
```

## Common mistakes

- **Using onRequest instead of onCall and then manually parsing auth tokens and handling CORS** — undefined Fix: Use onCall for endpoints called from Firebase client SDKs. It handles auth verification, CORS, and data serialization automatically. Use onRequest only for webhooks or third-party integrations that need raw HTTP access.
- **Throwing regular JavaScript errors instead of HttpsError, which causes the client to receive a generic 'internal' error** — undefined Fix: Always throw HttpsError with a specific code and message. Regular throws are caught by Firebase and converted to 'internal' errors, hiding the actual problem from the client.
- **Trusting client-sent data without validation, allowing invalid or malicious input** — undefined Fix: Validate every field in request.data on the server. Check types, lengths, and allowed values. The client can send anything — never trust it.
- **Forgetting to initialize firebase-admin before using Firestore or other admin services in the function** — undefined Fix: Call initializeApp() from firebase-admin/app at the top level of your functions file, before any function handlers. This only needs to be done once.

## Best practices

- Use onCall for client-facing functions and onRequest only for webhooks or external API integrations
- Always validate input data and throw HttpsError with specific codes for meaningful client-side error handling
- Check request.auth before performing any authenticated operation and throw 'unauthenticated' if missing
- Use the Admin SDK inside callable functions to bypass security rules and perform privileged operations securely
- Test functions locally with the Emulator Suite before deploying to avoid unnecessary deploys and cold start waiting
- For applications with many callable functions handling complex business logic, RapidDev can help structure your functions architecture and implement robust error handling patterns
- Deploy functions selectively by name to avoid accidentally updating unrelated functions

## Frequently asked questions

### What is the difference between onCall and onRequest?

onCall automatically handles authentication (verifies Firebase ID tokens), CORS headers, and JSON serialization. onRequest gives you a raw Express-compatible handler where you must handle all of these manually. Use onCall for client app endpoints and onRequest for webhooks or third-party APIs.

### Can I call a callable function without being authenticated?

Yes. If the caller is not signed in, request.auth will be null. Your function can check for this and either allow unauthenticated access or throw an 'unauthenticated' HttpsError.

### What are the timeout limits for callable functions?

V2 callable functions can run for up to 60 minutes (3600 seconds). V1 callable functions are limited to 9 minutes (540 seconds). The default timeout is 60 seconds for v2. Configure it in the function options: onCall({ timeoutSeconds: 300 }, handler).

### Can I use callable functions with App Check?

Yes. Callable functions automatically verify App Check tokens when App Check is enabled. Set the enforceAppCheck option to true to reject requests without valid App Check tokens.

### How do I return a large amount of data from a callable function?

Callable functions have a maximum response size of 10 MB. For larger data, write the data to Cloud Storage and return a signed URL, or paginate the response using offset and limit parameters.

### Can I call a callable function from another Cloud Function?

While possible, it is better to extract the shared logic into a regular async function and call it directly from both functions. This avoids unnecessary HTTP overhead and auth token verification between server-side functions.

### Do callable functions support streaming responses?

No. Callable functions return a single JSON response. For streaming, use an onRequest function with Server-Sent Events, or write incremental results to a Firestore document that the client listens to with onSnapshot.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-write-https-callable-function-in-firebase
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-write-https-callable-function-in-firebase
