Base44 apps handle real business processes: proposals, service agreements, intake forms, onboarding paperwork. None of that closes cleanly without a signature, and Base44 doesn't ship one. Firma.dev fills that gap with an e-signature API you can wire into any Base44 app in an afternoon, not a sprint.
How it works (without the technical details)
Firma.dev gives you two ways to connect a Base44 app, and which one you pick depends on how many apps you're running.
If it's a single app, you write a backend function: a small piece of TypeScript that calls Firma.dev's REST API to create a signing request, send it, and listen for the signature coming back. Base44 keeps your API key locked in a secret, so it never touches the browser.
If you're running a workspace with multiple apps, you register Firma.dev once as a custom integration using its OpenAPI spec. After that, every app in the workspace can call Firma.dev endpoints directly through the Base44 SDK, no separate function per app.
Either path ends the same way: a signer opens the document, signs it embedded right inside your app, and a webhook tells your backend the moment it's done.
What you need first
A Firma.dev account with an API key
A Base44 app on the Builder plan or higher (required for backend functions and custom integrations)
A Firma.dev template with signing fields already set up
Path 1: backend functions
This is the more flexible route, and the one to reach for if you're wiring up a single app.
Store the key. In the Base44 editor, go to Dashboard → Secrets, add a new secret, and name it FIRMA_API_KEY. Never put the key in frontend code, only backend functions should ever see it.
Write the function. Under Dashboard → Code → Functions, create a new function (or have the Base44 AI chat generate one). This is the exact example from Firma.dev's docs, creating and sending a signing request from a template in a single call:
import { createClientFromRequest } from "npm:@base44/sdk";
const FIRMA_API = "https://api.firma.dev/functions/v1/signing-request-api";
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me();
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const { template_id, signer_email, signer_first_name, signer_last_name } =
await req.json();
const apiKey = Deno.env.get("FIRMA_API_KEY");
const response = await fetch(
`${FIRMA_API}/signing-requests/create-and-send`,
{
method: "POST",
headers: {
Authorization: apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
template_id,
recipients: [
{
first_name: signer_first_name,
last_name: signer_last_name,
email: signer_email,
designation: "Signer",
order: 1,
},
],
}),
}
);
const data = await response.json();
if (!response.ok) {
return Response.json({ error: data }, { status: response.status });
}
return Response.json({
signing_request_id: data.id,
status: "sent",
});
});import { createClientFromRequest } from "npm:@base44/sdk";
const FIRMA_API = "https://api.firma.dev/functions/v1/signing-request-api";
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me();
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const { template_id, signer_email, signer_first_name, signer_last_name } =
await req.json();
const apiKey = Deno.env.get("FIRMA_API_KEY");
const response = await fetch(
`${FIRMA_API}/signing-requests/create-and-send`,
{
method: "POST",
headers: {
Authorization: apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
template_id,
recipients: [
{
first_name: signer_first_name,
last_name: signer_last_name,
email: signer_email,
designation: "Signer",
order: 1,
},
],
}),
}
);
const data = await response.json();
if (!response.ok) {
return Response.json({ error: data }, { status: response.status });
}
return Response.json({
signing_request_id: data.id,
status: "sent",
});
});import { createClientFromRequest } from "npm:@base44/sdk";
const FIRMA_API = "https://api.firma.dev/functions/v1/signing-request-api";
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me();
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const { template_id, signer_email, signer_first_name, signer_last_name } =
await req.json();
const apiKey = Deno.env.get("FIRMA_API_KEY");
const response = await fetch(
`${FIRMA_API}/signing-requests/create-and-send`,
{
method: "POST",
headers: {
Authorization: apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
template_id,
recipients: [
{
first_name: signer_first_name,
last_name: signer_last_name,
email: signer_email,
designation: "Signer",
order: 1,
},
],
}),
}
);
const data = await response.json();
if (!response.ok) {
return Response.json({ error: data }, { status: response.status });
}
return Response.json({
signing_request_id: data.id,
status: "sent",
});
});If you'd rather review a request before it goes out, Firma.dev's docs note the alternative: POST /signing-requests to create a draft, then POST /signing-requests/{id}/send separately.
Connect it to the UI. Call the function from the frontend with the Base44 SDK:
import { base44 } from "@/api/base44Client";
const response = await base44.functions.invoke("sendSigningRequest", {
template_id: "your-template-id",
signer_email: "alice@example.com",
signer_first_name: "Alice",
signer_last_name: "Johnson",
});import { base44 } from "@/api/base44Client";
const response = await base44.functions.invoke("sendSigningRequest", {
template_id: "your-template-id",
signer_email: "alice@example.com",
signer_first_name: "Alice",
signer_last_name: "Johnson",
});import { base44 } from "@/api/base44Client";
const response = await base44.functions.invoke("sendSigningRequest", {
template_id: "your-template-id",
signer_email: "alice@example.com",
signer_first_name: "Alice",
signer_last_name: "Johnson",
});Or just tell the Base44 AI chat what you want: "When the user clicks 'Send Contract', call the sendSigningRequest function with the template ID, signer email, and name from the form."
Track completions with a webhook. Create a backend function to receive the event, then register it in Firma.dev under Settings → Webhooks, pointing at https://<your-app-domain>/functions/<function-name>.
import { createClientFromRequest } from "npm:@base44/sdk";
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
const payload = await req.json();
const { type, data } = payload;
if (type === "signing_request.completed") {
const signingRequestId = data.signing_request.id;
}
return Response.json({ received: true });
});import { createClientFromRequest } from "npm:@base44/sdk";
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
const payload = await req.json();
const { type, data } = payload;
if (type === "signing_request.completed") {
const signingRequestId = data.signing_request.id;
}
return Response.json({ received: true });
});import { createClientFromRequest } from "npm:@base44/sdk";
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
const payload = await req.json();
const { type, data } = payload;
if (type === "signing_request.completed") {
const signingRequestId = data.signing_request.id;
}
return Response.json({ received: true });
});Path 2: workspace-level OpenAPI integration
Running more than one Base44 app? Set this up once and every app in the workspace inherits it.
From your profile icon, go to Settings → Integrations → New Integration → From URL, and paste Firma.dev's OpenAPI spec URL (check Firma.dev's API changelog for the current version). Pick the endpoints you need, up to 30, commonly:
POST /signing-requests — create a signing request from a template or document
POST /signing-requests/create-and-send — create and send in one call
GET /signing-requests/{id} — check status
GET /signing-requests — list signing requests
POST /templates/{id}/duplicate — duplicate a template into a signing request
GET /templates — list available templates
For authentication, set the base URL to https://api.firma.dev/functions/v1/signing-request-api, then add a custom header named Authorization with your Firma.dev API key as the value. Base44 encrypts and stores it at the workspace level, it never reaches the browser.
From there, any app calls Firma.dev directly:
const result = await base44.integrations.custom.call(
"firma",
"post:/signing-requests/create-and-send",
{
payload: {
template_id: templateId,
recipients: [
{
first_name: "Alice",
last_name: "Johnson",
email: "alice@example.com",
designation: "Signer",
order: 1,
},
],
},
}
);const result = await base44.integrations.custom.call(
"firma",
"post:/signing-requests/create-and-send",
{
payload: {
template_id: templateId,
recipients: [
{
first_name: "Alice",
last_name: "Johnson",
email: "alice@example.com",
designation: "Signer",
order: 1,
},
],
},
}
);const result = await base44.integrations.custom.call(
"firma",
"post:/signing-requests/create-and-send",
{
payload: {
template_id: templateId,
recipients: [
{
first_name: "Alice",
last_name: "Johnson",
email: "alice@example.com",
designation: "Signer",
order: 1,
},
],
},
}
);Ask the Base44 AI chat to wire up Firma.dev calls here too, it detects the workspace integration and uses it on its own.
Embedded signing
For apps where you want the signer to complete the document without leaving your UI, pull the signing_request_user_id from the API response and load it in an iframe:
<iframe
src="https://app.firma.dev/signing/{signing_request_user_id}"
style="width:100%;height:900px;border:0;"
allow="camera;microphone;clipboard-write"
title="Document Signing"
></iframe><iframe
src="https://app.firma.dev/signing/{signing_request_user_id}"
style="width:100%;height:900px;border:0;"
allow="camera;microphone;clipboard-write"
title="Document Signing"
></iframe><iframe
src="https://app.firma.dev/signing/{signing_request_user_id}"
style="width:100%;height:900px;border:0;"
allow="camera;microphone;clipboard-write"
title="Document Signing"
></iframe>What you get out of the box
Every signing request built this way inhertis Firma.dev's actual product, not a stripped-down API wrapper: an embeddable template editor for building documents, Customer Workspaces if you're running a multi-tenant Base44 app, and legal validity in 55+ countries under SES and AES (eIDAS-aligned; Firma.dev doesn't offer QES yet).
The pricing makes sense for builders
Base44 apps are usually early-stage, which makes per-seat signing tools a bad fit before you even have a full team. Firma.dev prices per envelope instead: €0.049 (~5¢), pay-as-you-go, no monthly minimum, no contract. Against DocuSign's per-user pricing, that's 97-99% cheaper at any volume you're likely to hit while building.
Send this to your developer (or do it yourself)
Both paths above are copy-pasteable, and most Base44 builders will have this running in under an hour. If you want the AI chat to do the wiring, point it at Firma.dev's Base44 integration guide and it can generate the functions directly. Firma.dev also has a Docs MCP server you can add under Account Settings → MCP Connections so the AI chat can pull accurate API details while you build.
Get started with Firma.dev for free, no credit card required.