Skip to main content
RapidDev - Software Development Agency
firebase-tutorial

How to Write HTTPS Callable Functions in Firebase

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.

What you'll learn

  • How to create a v2 callable function with onCall and handle authentication context
  • How to call the function from the client using httpsCallable
  • How to validate input data and throw typed errors with HttpsError
  • How to test callable functions locally with the Emulator Suite
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read15-20 minFirebase Cloud Functions v2, Firebase JS SDK v9+, Node.js 18+, Blaze plan requiredMarch 2026RapidDev Engineering Team
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.

Prerequisites

  • 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.

typescript
1// functions/src/index.ts
2import { onCall, HttpsError } from 'firebase-functions/v2/https'
3
4export const greetUser = onCall((request) => {
5 // request.auth is populated if the caller is authenticated
6 if (!request.auth) {
7 throw new HttpsError('unauthenticated', 'You must be signed in')
8 }
9
10 const name = request.data.name
11 if (!name || typeof name !== 'string') {
12 throw new HttpsError('invalid-argument', 'Name is required')
13 }
14
15 return {
16 message: `Hello, ${name}! Your UID is ${request.auth.uid}`
17 }
18})

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.

typescript
1// src/lib/functions.ts
2import { getFunctions, httpsCallable } from 'firebase/functions'
3
4const functions = getFunctions()
5
6export async function greetUser(name: string) {
7 const greet = httpsCallable(functions, 'greetUser')
8 const result = await greet({ name })
9 return result.data as { message: string }
10}
11
12// Usage in a component
13const response = await greetUser('Alice')
14console.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.

typescript
1// functions/src/index.ts
2import { onCall, HttpsError } from 'firebase-functions/v2/https'
3
4interface CreatePostData {
5 title: string
6 body: string
7 category: string
8}
9
10export const createPost = onCall(async (request) => {
11 if (!request.auth) {
12 throw new HttpsError('unauthenticated', 'Sign in required')
13 }
14
15 const { title, body, category } = request.data as CreatePostData
16
17 if (!title || title.length > 200) {
18 throw new HttpsError('invalid-argument', 'Title must be 1-200 characters')
19 }
20 if (!body || body.length > 10000) {
21 throw new HttpsError('invalid-argument', 'Body must be 1-10000 characters')
22 }
23 if (!['tech', 'design', 'business'].includes(category)) {
24 throw new HttpsError('invalid-argument', 'Invalid category')
25 }
26
27 // Use Admin SDK to write to Firestore (bypasses security rules)
28 const { getFirestore } = await import('firebase-admin/firestore')
29 const db = getFirestore()
30
31 const docRef = await db.collection('posts').add({
32 title,
33 body,
34 category,
35 authorId: request.auth.uid,
36 createdAt: new Date()
37 })
38
39 return { postId: docRef.id }
40})

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.

typescript
1import { getFunctions, httpsCallable } from 'firebase/functions'
2import { FirebaseError } from 'firebase/app'
3
4const functions = getFunctions()
5
6async function submitPost(title: string, body: string, category: string) {
7 const createPost = httpsCallable(functions, 'createPost')
8
9 try {
10 const result = await createPost({ title, body, category })
11 const { postId } = result.data as { postId: string }
12 console.log('Post created:', postId)
13 return postId
14 } catch (error) {
15 if (error instanceof FirebaseError) {
16 switch (error.code) {
17 case 'functions/unauthenticated':
18 alert('Please sign in to create a post')
19 break
20 case 'functions/invalid-argument':
21 alert(error.message)
22 break
23 default:
24 alert('Something went wrong. Please try again.')
25 }
26 }
27 throw error
28 }
29}

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.

typescript
1// In your client initialization code
2import { getFunctions, connectFunctionsEmulator } from 'firebase/functions'
3
4const functions = getFunctions()
5
6if (import.meta.env.DEV) {
7 connectFunctionsEmulator(functions, '127.0.0.1', 5001)
8}
9
10// Start the emulator:
11// 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.

typescript
1firebase deploy --only functions:greetUser,functions:createPost

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

Complete working example

functions/src/index.ts
1// functions/src/index.ts
2import { onCall, HttpsError } from 'firebase-functions/v2/https'
3import { initializeApp } from 'firebase-admin/app'
4import { getFirestore } from 'firebase-admin/firestore'
5
6initializeApp()
7const db = getFirestore()
8
9// Simple callable function with auth check
10export const greetUser = onCall((request) => {
11 if (!request.auth) {
12 throw new HttpsError('unauthenticated', 'You must be signed in')
13 }
14 const name = request.data.name
15 if (!name || typeof name !== 'string') {
16 throw new HttpsError('invalid-argument', 'Name is required')
17 }
18 return { message: `Hello, ${name}!`, uid: request.auth.uid }
19})
20
21// Callable function with Firestore write
22export const createPost = onCall(async (request) => {
23 if (!request.auth) {
24 throw new HttpsError('unauthenticated', 'Sign in required')
25 }
26
27 const { title, body, category } = request.data
28 if (!title || typeof title !== 'string' || title.length > 200) {
29 throw new HttpsError('invalid-argument', 'Title must be 1-200 chars')
30 }
31 if (!body || typeof body !== 'string' || body.length > 10000) {
32 throw new HttpsError('invalid-argument', 'Body must be 1-10000 chars')
33 }
34
35 const docRef = await db.collection('posts').add({
36 title,
37 body,
38 category: category || 'general',
39 authorId: request.auth.uid,
40 authorEmail: request.auth.token.email || null,
41 createdAt: new Date(),
42 updatedAt: new Date()
43 })
44
45 return { postId: docRef.id }
46})
47
48// Callable function to delete own post
49export const deletePost = onCall(async (request) => {
50 if (!request.auth) {
51 throw new HttpsError('unauthenticated', 'Sign in required')
52 }
53
54 const { postId } = request.data
55 if (!postId || typeof postId !== 'string') {
56 throw new HttpsError('invalid-argument', 'Post ID is required')
57 }
58
59 const postRef = db.collection('posts').doc(postId)
60 const post = await postRef.get()
61
62 if (!post.exists) {
63 throw new HttpsError('not-found', 'Post does not exist')
64 }
65 if (post.data()?.authorId !== request.auth.uid) {
66 throw new HttpsError('permission-denied', 'You can only delete your own posts')
67 }
68
69 await postRef.delete()
70 return { deleted: true }
71})

Common mistakes when writing HTTPS Callable Functions in Firebase

Why it's a problem: Using onRequest instead of onCall and then manually parsing auth tokens and handling CORS

How to avoid: 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.

Why it's a problem: Throwing regular JavaScript errors instead of HttpsError, which causes the client to receive a generic 'internal' error

How to avoid: 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.

Why it's a problem: Trusting client-sent data without validation, allowing invalid or malicious input

How to avoid: Validate every field in request.data on the server. Check types, lengths, and allowed values. The client can send anything — never trust it.

Why it's a problem: Forgetting to initialize firebase-admin before using Firestore or other admin services in the function

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Show me how to write a Firebase Cloud Functions v2 callable function using onCall that receives a title and body, validates input, writes to Firestore with the Admin SDK, and returns the new document ID. Include the client-side code to call it with httpsCallable and handle errors.

Firebase Prompt

Create a set of Firebase callable functions for a blog app: createPost (validates title/body, writes to Firestore), deletePost (checks ownership before deleting), and getPost (fetches a post by ID). Use onCall v2 syntax with HttpsError for validation. Include the client-side caller functions with TypeScript types.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.