Everything you need to use Dbrij Forms from outside the dashboard: links and prefill, embedding, the public form API, spam protection, signed webhooks, the live spreadsheet feed, exports, custom domains and plan limits.
Dbrij Forms builds forms whose answers become work: a task with an owner, a lead, a ticket, a candidate. Forms take files, payments, signatures and approvals, confirm who is answering, collect with no signal in the mobile app, and speak more than one language.
This reference covers everything that touches a form from outside the dashboard: its public link and prefill, embedding it in your own site, the public API the form page itself uses, spam protection, webhooks, the live spreadsheet feed, exports, custom domains and the limits of each plan.
https://api.dbrij.com/api/v1/public/formsThe public API needs no key: it is what the hosted page and the embed call for somebody with no account. JSON responses come inside an envelope, { "success": true, "data": … }; errors come as { "success": false, "statusCode": 400, "message": "…" }, and the message is written to be shown to the person answering. The spreadsheet feed is plain CSV.
Every published form has a public address, /f/<slug> on Dbrij, such as https://app.dbrij.com/f/job-application. Copy the exact link from the form's Share panel. A form that has closed (past its closing time, or full) still opens and says why, so somebody holding a printed flyer is not told the company does not exist. A form that has never been published answers 404.
Add a question's key as a query parameter and the question starts with that answer. The key is shown on each question in the editor.
https://app.dbrij.com/f/job-application?full_name=Ada%20Obi&role=engineer&skills=design,research&ref=newsletter?embed=1 drops the page's outer chrome for use inside a frame, and makes the form tell its parent page how tall it is (see Embedding).
When a form has save and resume switched on, the person can save their answers and come back from a link, /f/<slug>?resume=<token>, optionally sent to their email. A draft can be finished for 30 days after its last save. Drafts are never counted, routed or billed as responses. Anyone with the link can see the saved answers, so the email says to keep it private.
A form with a password shows its name and who is asking, and none of its questions, until the password is given. The page trades the password for an access token that lasts six hours (see POST /:slug/unlock) and keeps it in the browser. An integration passes the token as ?access=<token> when reading the form and as accessToken when saving or submitting.
Paste one script tag where the form should appear. By default it renders a button; pressing it opens the form over your page in a centred window. The form itself loads only when somebody asks for it. The Share panel gives you the tag with your form's slug filled in.
<script src="https://app.dbrij.com/forms-embed.js" data-form="job-application" data-label="Apply now" async></script><script src="https://app.dbrij.com/forms-embed.js" data-form="job-application" data-mode="inline" data-height="720" async></script>ATTRIBUTES
| data-form* | string | The form's slug: 4 to 64 letters, digits, underscores or hyphens. |
| data-label | string | The button's words, and the window's accessible name. Default "Open form". |
| data-color | string | The button's colour, such as #0ea5e9. Default Dbrij red. |
| data-mode | "inline" | Put the form itself in the page instead of a button. It grows to fit as the form reports its height. |
| data-height | number | The inline form's height in pixels until the first height message arrives. Default 640. |
The tag is small and has no dependencies: no globals, inline styles only, and every element it adds is prefixed dbrij-form. Several tags on one page each render their own form, and each tag runs once. The window closes on Escape, on a click outside it, on its close button, and on a close message from the form.
Framed with ?embed=1, the form page posts messages to its parent with postMessage. The embed believes a message only when it comes from its own script's origin and from the frame it made, so another frame on your page cannot resize or close it. Check both too if you listen yourself.
| dbrij-form-height | { type, height } | Sent whenever the form's height changes, in pixels. The inline embed sets the frame to it, kept between 120 and 20,000. |
| dbrij-form-close | { type } | The popup window closes when the framed page posts it. A form opened in the popup offers a Close button on its thank you page that posts it; a page of your own framed the same way can post it too. |
<iframe id="dbrij-form-job-application" src="https://app.dbrij.com/f/job-application?embed=1" title="Job application"
style="width:100%;height:640px;border:0" loading="lazy"></iframe>
<script>
window.addEventListener("message", function (e) {
var f = document.getElementById("dbrij-form-job-application");
if (!f || e.origin !== "https://app.dbrij.com" || e.source !== f.contentWindow) return;
if (e.data && e.data.type === "dbrij-form-height" && e.data.height > 0) f.style.height = e.data.height + "px";
});
</script>These are the calls the hosted page and the embed make. Use them to put a form behind your own interface. No key is needed; each route has its own limit of requests a minute from one address, and going over it answers 429.
https://api.dbrij.com/api/v1/public/formsGET/public/forms/:slug
The form as the page renders it: its settings, its questions (never the right answers or hidden formulas), branding and whether it is closed. 60 a minute.
QUERY
| access | string | The token from unlock, for a password protected form. Without it a locked form comes back with passwordRequired: true and no questions. |
| resume | string | A resume token. The saved answers come back as resumed, while the draft is under 30 days old. |
{
"slug": "job-application",
"name": "Job application",
"description": "Tell us about yourself.",
"companyName": "Acme",
"schema": { "pages": [ { "id": "p1", "fields": [ { "key": "full_name", "type": "short_text", "label": "Your name", "required": true } ] } ] },
"settings": { "allowPrefill": true, "saveAndResume": true, "welcome": null, "thankYou": null, "translations": {}, "quiz": null },
"passwordRequired": false,
"captchaSiteKey": null,
"requireVerification": false,
"verificationChannel": "email",
"payment": null,
"resumed": null,
"badge": true,
"closedReason": null
}POST/public/forms/:slug/unlock
Trades the password for an access token that opens this one form for six hours. 5 a minute, because it is the one thing that can be guessed. A wrong password answers 400 and never says how close it was.
{ "password": "spring-intake" }{ "accessToken": "…" }POST/public/forms/:slug/progress
Only on a form with save and resume switched on. Saving again with the same resumeToken updates the draft. 20 a minute.
| answers* | object | Answers so far, keyed by question key. |
| resumeToken | string | The draft to update. Leave it out to start one. |
| string | Send the resume link here. | |
| language | string | The language being answered in, when the form has translations. |
| accessToken | string | Required on a password protected form. |
{ "resumeToken": "9f2c…", "resumeUrl": "https://app.dbrij.com/f/job-application?resume=9f2c…", "emailed": true }POST/public/forms/:slug/submit
Answers are keyed by question key. Keys the form does not ask are removed, and the rest are checked by the same rules the page uses (required questions, conditions, formats): a problem answers 400 with every problem in the message. 10 a minute.
| answers* | object | Answers keyed by question key. Up to 256 KB of JSON. A file question's answer is the url from an upload ticket. |
| clientId | string | Your own id for this submission, up to 64 characters. Sending it again returns the first response instead of a second one, so a retry is safe. |
| elapsedMs | integer | Milliseconds from opening the form to submitting. See Spam protection. |
| website | string | The hidden field. Always send it empty. See Spam protection. |
| captchaToken | string | The human check's token, when the form view carried a captchaSiteKey. |
| verificationToken | string | From verify/confirm, on a form that confirms who is answering. |
| accessToken | string | From unlock, on a password protected form. |
| resumeToken | string | The draft this submission finishes. The draft is removed once the response is in. |
| language | string | The language answered in. Kept only when the form has that translation. |
| offline | boolean | Answers collected with no signal in the mobile app and sent now. Needs a plan with offline collection. |
{
"answers": { "full_name": "Ada Obi", "email": "ada@example.com", "role": "engineer", "cv": "https://…/forms/…/a1b2c3.pdf" },
"clientId": "6f1d0c2a-attempt-1",
"elapsedMs": 48210,
"website": ""
}{
"responseId": "3c9e…",
"status": "complete",
"message": "Thank you. We will be in touch.",
"paymentUrl": null
}STATUS
| complete | status | In, and sent on to its rules, notifications and webhooks. |
| awaiting_payment | status | The form charges. Send the person to paymentUrl; the response goes on once paid. |
| awaiting_approval | status | Waiting for somebody at the company to approve it. |
| awaiting_signature | status | Waiting for the person to sign the record of their answers. |
The result may also carry redirectUrl (where to send them next) and, on a quiz that shows it, score with the score, the maximum, a percentage and an outcome.
POST/public/forms/:slug/verify
On a form that confirms who is answering, so that one person is one response. The code goes by email or by SMS, as the form is set. A code lasts 10 minutes and allows 5 tries, and one address gets at most 5 codes an hour on one form. 5 a minute.
{ "identity": "ada@example.com" }{ "channel": "email", "sentTo": "a••@example.com", "expiresIn": 600 }POST/public/forms/:slug/verify/confirm
Returns a token to send with the answers as verificationToken. It works once, and the person it proves can answer the form once. 10 a minute.
{ "identity": "ada@example.com", "code": "481920" }{ "verificationToken": "…" }POST/public/forms/:slug/upload
Nobody needs an account to attach a file. Ask for a ticket for one file question, PUT the file to putUrl within 15 minutes, then send url as that question's answer. The extension must be one the question accepts. When the company's file storage is full the ticket is refused with "This form is not taking files right now." 20 a minute.
{ "fieldKey": "cv", "ext": "pdf" }{ "putUrl": "https://…", "fileKey": "forms/…/a1b2c3.pdf", "url": "https://…/forms/…/a1b2c3.pdf" }await fetch(ticket.putUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type || 'application/octet-stream' } });
answers.cv = ticket.url;POST/public/forms/:slug/track
Feeds the drop off report: where people stop. Always answers 204, whatever happens. 120 a minute.
| sessionKey* | string | A random id for this visit, up to 64 characters. Never tied to an account. |
| type* | string | started, field_seen, field_done, submitted or abandoned. |
| fieldKey | string | The question it is about. |
| dwellMs | integer | Milliseconds spent on that question, up to one hour. |
Every form has two quiet checks, and can add a third.
| Hidden field | website | The page carries a field people never see. Anything in it means a script filled the form. |
| Timing | elapsedMs | Submitted less than one second after the form opened: nobody read those questions. |
| Human check | captchaToken | Optional, switched on per form. |
A submission caught by the hidden field or the timing check is thanked and dropped: it gets an ordinary looking success with a random response id, and nothing is stored. The script learns nothing it could tune itself against. Answers collected offline skip the timing check.
When a form asks for one, the form view carries captchaSiteKey. Render the check with that key and send its token as captchaToken; a failed check answers 400 "Please finish the check that shows you are a person, then send again." If the checker itself cannot be reached, the answer goes through rather than being lost. The check runs only on Dbrij's own address: on a custom domain the form view carries no key and the hidden field and timing check stand alone.
Send every response to your own system. Add a connection from the form's Connect panel: a webhook (your own https address) or a team chat channel (paste the incoming webhook URL your team chat app gives you for that channel). A form sends to up to 10 places. A webhook address must start with https:// and be on the public internet.
A delivery goes out when a response is complete. A response held for payment, approval or a signature goes out once it is through, never before. Each webhook gets a signing secret, whsec_…, shown once when you create it: keep it then.
POST /your/endpoint
content-type: application/json
x-dbrij-event: form.response
x-dbrij-signature: sha256=<hex>
x-dbrij-signature-v2: t=<unix seconds>,sha256=<hex>{
"type": "form.response",
"form": { "id": "f1a2…", "name": "Job application", "slug": "job-application" },
"response": {
"id": "3c9e…",
"submittedAt": "2026-11-03T09:14:22.000Z",
"answers": { "full_name": "Ada Obi", "role": "engineer" },
"readable": { "Your name": "Ada Obi", "Role": "Engineer" },
"score": null,
"maxScore": null,
"language": null
}
}answers is keyed by question key, exactly as stored. readable is the same answers as people read them, keyed by the question's words, with empty answers left out.
Both signatures are an HMAC SHA256 of the raw request body with your secret. Prefer the second: it signs the timestamp with the body, so a captured delivery cannot be replayed later. Compute over the exact bytes you received, before any JSON parsing.
import { createHmac, timingSafeEqual } from 'node:crypto';
const same = (a, b) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
// x-dbrij-signature-v2: "t=1767430462,sha256=…"
function verifyV2(rawBody, header, secret, toleranceSec = 300) {
const [tPart, sigPart] = header.split(',');
const t = tPart.slice(2);
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
const expected = 'sha256=' + createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
return same(sigPart, expected);
}
// x-dbrij-signature: "sha256=…"
function verify(rawBody, header, secret) {
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
return same(header, expected);
}test, built from the form's questions.A team chat connection posts a message with the form's name, up to 12 answers and a link to the response in Dbrij. It is not signed.
A form can also email up to 10 people at the company for each new response, and send the person who answered a confirmation from the company, with their answers if you choose. Both are set in the form's settings.
Turn on the live spreadsheet link in the form's Connect panel and you get a secret address that always answers with the latest responses as CSV. Spreadsheet apps can import it from the web (with an IMPORTDATA formula, or a data import from a web address) and refresh it on their own schedule.
GET/public/forms/feed/<token>.csv
https://api.dbrij.com/api/v1/public/forms/feed/<token>.csvResponse id, Submitted (ISO 8601, UTC), Status, Respondent (the confirmed email or phone, when the form confirms who is answering), Collected offline, Arrived late, then Score on a quiz and Language when anyone answered in a translation, then one column per question, headed with the question's words. Several answers to one question are joined with ; .
Response id,Submitted,Status,Respondent,Collected offline,Arrived late,Your name,Role
3c9e…,2026-11-03T09:14:22.000Z,complete,,no,no,Ada Obi,engineerFrom a form's responses in the dashboard you can download:
The CSV, the .xlsx file and the live spreadsheet feed are written from one table, so their columns always match (see Live spreadsheet feed).
Serve your forms from your own domain, https://forms.yourcompany.com/<slug>, instead of Dbrij's address. Custom domains come with the Starter plan and up. A company can use up to 5, a domain can belong to one company only, and Dbrij's own domains cannot be used.
Add the domain from a form's Share panel, then publish two records at your DNS host. The panel shows the exact values for your domain.
| TXT | _dbrij-forms.<domain> | Value dbrij-forms=<token>. Proves the domain is yours. |
| CNAME | <domain> | Points the domain at Dbrij: forms.dbrij.com, or the target the panel shows. |
_dbrij-forms.forms.yourcompany.com. TXT "dbrij-forms=4b1e…"
forms.yourcompany.com. CNAME forms.dbrij.com.Then choose Verify. The TXT record is checked first, then the CNAME, and the message says which one is missing. Records can take a few minutes to appear. Once verified, the domain serves your company's forms at /<slug> and a certificate is issued for it on the first visit; a domain never serves another company's form.
On a custom domain the human check is not offered; the hidden field and the timing check still apply (see Spam protection).
Forms is billed per company, monthly or yearly; a year costs ten months. The limits below are the ones the server enforces.
| Plan | Monthly | Responses a month | Live forms | File storage | Rules per form | Payments | Confirm who answers | Offline collection | No Dbrij badge | Custom domain |
|---|---|---|---|---|---|---|---|---|---|---|
| Free | Free | 500 | 3 | 100 MB | 1 | No | No | No | No | No |
| Starter | ₦8,000 | 5,000 | 25 | 5 GB | 10 | Yes | Yes | Yes | Yes | Yes |
| Growth | ₦25,000 | 50,000 | 500 | 50 GB | 50 | Yes | Yes | Yes | Yes | Yes |
| Answers in one response | 256 KB | Of JSON, after unknown keys are removed. |
| Connections per form | 10 | Webhooks and team chat channels together. |
| People told about each response | 10 | Email addresses in the form's settings. |
| Other languages per form | 8 | Translations besides the form's own language. |
| Custom domains per company | 5 | Starter plan and up. |
| Spreadsheet feed | 5,000 rows | The newest responses. |
| Draft kept | 30 days | From the last save. |
| Access token | 6 hours | From unlocking a password protected form. |
| Confirmation code | 10 minutes, 5 tries | At most 5 codes an hour to one address on one form. |
Every SDK and package for Forms, with what it does, how to install it and the smallest working use. The whole catalogue, for every product, is on the apps page.
Put a Dbrij form on any website with one script tag, as a button that opens it or inline in the page.
Script for your site · JavaScript · Served by Dbrij · Runs on Browsers
<script src="https://app.dbrij.com/forms-embed.js" data-form="job-application" data-label="Apply now" async></script><script src="https://app.dbrij.com/forms-embed.js" data-form="job-application" data-mode="inline" data-height="720" async></script>Dbrij is Nigeria’s company operating system: the first Nigerian built platform to put team chat, video meetings, email, HR, tax compliant payroll, customer support, marketing, analytics and cloud hosting in one product on one subscription.