SnagBane — Standalone forms for Microsoft 365 & Google
Standalone: SnagBane runs on your own server and replaces Microsoft Forms and Google Forms without depending on them — forms, answers and files stay in your database. The free Community edition is the complete app (builder with branching and calculations, portals, approvals, workspaces, email, webhooks, Power Automate / Zapier / Make / n8n and most integrations) and needs no Microsoft or Google account. Enterprise connects Microsoft 365, Google Workspace and Okta: sign-in, live directory person pickers and prefill, groups, Teams apps, SharePoint, Excel, Google Sheets and Drive, and Outlook / Gmail mail. Sections about Microsoft 365, Google Workspace and Okta apply to Enterprise.
Requirements
| Component | Minimum |
|---|---|
| PHP | 8.3 or newer with extensions: pdo, mbstring, openssl, tokenizer, xml, ctype, json, fileinfo, curl |
| Database | MySQL 8 / MariaDB 10.6+ (recommended), PostgreSQL 14+, or SQLite 3.35+ |
| Web server | Apache (with mod_rewrite) or Nginx, with HTTPS (required by Microsoft for sign-in redirect URIs other than localhost) |
| Outbound HTTPS | To login.microsoftonline.com and graph.microsoft.com for Microsoft 365 features |
The download includes vendor/ and pre-built front-end assets. You don't need Composer or Node.js.
Install on cPanel / shared hosting
- Create a MySQL database and user in cPanel → Databases → Manage My Databases (cPanel 118 and earlier: MySQL® Databases): under Create New Database create the database; under Add New User create a user (use Password Generator and copy the password); under Add User To Database add the user with ALL PRIVILEGES. cPanel puts your account name in front of both names (e.g.
youraccount_snagbane). The installer shows these steps too. To change the password later, change it in cPanel (Current Users → Change Password) and put the same password inDB_PASSWORDin.env, or runphp artisan snagbane:db-password, which changes both. - Upload
snagbane.zipwith File Manager and extract it, for example into/home/you/snagbane. - Point your (sub)domain's document root to
/home/you/snagbane/public. If your host doesn't allow that, move the contents ofpublic/intopublic_html/and fix the two paths inpublic_html/index.phpto point at../snagbane/. - Open
https://your-domain/installand follow the three steps: requirements, database, admin account. - Add one cron job in cPanel → Cron Jobs (every minute):
cd /home/you/snagbane && php artisan schedule:run >/dev/null 2>&1— approval reminders, the nightly directory copy for fast person search and clean-up run from it. - Sign in and go to Administration → Microsoft 365.
Install on a VPS (Ubuntu + Nginx)
sudo apt install nginx php8.3-fpm php8.3-{mysql,mbstring,xml,curl,zip,intl,sqlite3} mariadb-server unzip
sudo unzip snagbane.zip -d /var/www/snagbane && sudo chown -R www-data: /var/www/snagbane
# nginx: root /var/www/snagbane/public; try_files $uri $uri/ /index.php?$query_string;
sudo certbot --nginx -d forms.example.com
echo '* * * * * www-data cd /var/www/snagbane && php artisan schedule:run >/dev/null 2>&1' | sudo tee /etc/cron.d/snagbane
Then browse to /install.
Install with Docker (recommended)
The stack runs SnagBane (Apache + PHP 8.3), a background worker for emails / webhooks / integrations, a scheduler (approval reminders, clean-up), MariaDB and Mailpit for testing email.
docker compose up -d --build
# open http://localhost:8080/install
# Database: MariaDB · host db · database snagbane · user snagbane · leave the password empty
# test inbox for all outgoing email: http://localhost:8025 (in Admin → Email use SMTP host "mailpit", port 1025, no encryption)
No default database passwords. During installation SnagBane replaces the database's starting password with a strong random password for this installation and keeps it in storage/.env (in the storage volume). MariaDB's root password is random too (SnagBane doesn't need it; it is printed once: docker compose logs db | grep "GENERATED ROOT PASSWORD"). See the password with docker compose exec app php artisan snagbane:db-password --show; change it with docker compose exec app php artisan snagbane:db-password (random, or --password=…), then docker compose restart.
Set SNAGBANE_PORT (default 8080) and SNAGBANE_MAIL_PORT (Mailpit, default 8025) in a .env file next to docker-compose.yml if those ports are taken, e.g. by a second copy of SnagBane. Everything that must survive upgrades lives in the storage and dbdata volumes; the exports/ folder receives Local folder exports. Upgrading: replace the files, then docker compose up -d --build — migrations run automatically.
Run locally on Windows (for testing, demos and recording videos)
The easiest way is Laragon (free):
- Install Laragon Full, which includes PHP 8.3, MySQL and Apache.
- Copy the SnagBane folder into
C:\laragon\www\snagbane. - Start Laragon. It serves the site at
http://snagbane.test. - Open
http://snagbane.test/install, pick SQLite and tick Import demo forms and responses.
You can also double-click start-local.bat if PHP is on your PATH. It serves at http://127.0.0.1:8000.
http://localhost redirect URIs for sign-in, so SSO works locally if you open SnagBane at http://localhost:8000.Updating
One click: Administration → System → Updates. SnagBane checks for a new version once a day (or click Check for updates); the Owner clicks Update now. SnagBane then:
- downloads the new version and checks its digital signature (a package that isn't signed by SnagBane is refused);
- saves a backup of the current version (Administration → System → Restore previous version undoes the update);
- shows a short maintenance page, copies the new files and updates the database. Files that the new version no longer has are removed from SnagBane's own code folders (
app,config,enterprise,lang,resources,routes,vendor,public/build,public/docsanddatabase/migrations,seeders,factories), so leftovers can't break it; your own files elsewhere (for example inpublic/) stay. Your.env,storage/, uploads and data are never touched.
- Shared hosting / cPanel: works as-is; the web server needs write access to the SnagBane folder.
- Docker: the update is also kept in the storage volume and re-applied when the containers restart. When you later pull or build a newer image, the image wins.
- If the page lost the connection during the update (a proxy or load balancer timed out the long request), the site may show the maintenance page for a moment: SnagBane finishes the update by itself within a few minutes (the scheduler runs every minute). To finish it right away, run
php artisan snagbane:update --finishin the SnagBane folder (Docker:docker compose exec app php artisan snagbane:update --finish). - Command line:
php artisan snagbane:update(or--checkto only look). - Enterprise updates need an active licence. When the licence has ended, the version you have keeps working — you just don't get new versions until you renew.
- Manual: back up
.env,storage/and the database, upload the new files over the old ones (keep.envandstorage/; best replace the code folders listed above completely, so no old files are left), then runphp artisan migrate --forceor click System → Clear cache.
Connect your Microsoft 365 tenant
Go to Administration → Microsoft 365 and choose the features (sign-in is always on; tick Person picker & profile prefill, and optionally groups and mail). SnagBane needs an app registration in your own tenant. The recommended way creates it with Microsoft's own tooling under your admin account — nothing passes through a third party.
PowerShell (recommended): the PowerShell tab shows New-SnagBaneApp.ps1 for the features you ticked (copy or download it so your security team can review it). Run it in Windows PowerShell or PowerShell 7 as a Global Administrator (or Application Administrator + Privileged Role Administrator). It needs the Microsoft.Graph module (Install-Module Microsoft.Graph -Scope CurrentUser), signs you in with your normal browser (MFA, Security defaults and Conditional Access work), creates the SnagBane Forms app registration, a 24-month client secret and admin consent, and prints three values. Paste them into the Manual tab and click Save & test.
Manual (Entra admin center): create an app registration yourself with the redirect URI shown in the wizard, add a client secret, add the Microsoft Graph permissions listed there and click Grant admin consent. Then paste the tenant ID, client ID and secret into SnagBane and click Save & test.
SnagBane only stores the app's tenant ID, client ID and client secret, and the secret is encrypted with your APP_KEY.
Other ways: Sign in with Microsoft
Under Other ways, SnagBane can create the app itself after a one-time admin sign-in through Microsoft's public Microsoft Graph Command Line Tools client:
- Sign in with Microsoft. A new tab opens Microsoft's normal sign-in page. Sign in as a Global Administrator (or Application Administrator + Privileged Role Administrator) and accept the permissions.
- Paste the address. The tab ends on an error page such as “This site can’t be reached” (address
http://localhost:1/?code=…) — that is expected: the browser never connects, so nothing on your computer receives the code. Click its address bar, copy the whole address (Ctrl+L, Ctrl+C; on a Mac ⌘L, ⌘C), paste it into SnagBane and click Continue. The address contains a one-time sign-in code; the full address or just the part after?both work. - Create the app. SnagBane then creates the SnagBane Forms app registration and enterprise app, adds a 24-month client secret, and grants admin consent for exactly the permissions you selected. You see each step as it completes.
The admin's sign-in token is used once and then thrown away. SnagBane only stores the new app's tenant ID, client ID and client secret, and the secret is encrypted with your APP_KEY.
How it works: the sign-in uses Microsoft's first-party Microsoft Graph Command Line Tools client with the authorization-code flow and PKCE (no client secret). That client only accepts Microsoft's registered return addresses (such as http://localhost), not your server's, which is why you copy the address instead of being sent back automatically. The code is useless without a secret that never leaves your SnagBane server, works once, expires after a few minutes and only for the administrator who started the sign-in.
Why not device code? The Device code tab (under Other ways) still exists, but Microsoft now blocks device-code sign-in in most tenants: Security defaults (on by default in new tenants) block it with error AADSTS530035, and the Microsoft-managed Conditional Access policy Block device code flow does the same (AADSTS53003). SnagBane recognises these errors and sends you to Sign in with Microsoft.
Permissions explained
| Feature | Permission | Type | Why |
|---|---|---|---|
| Sign in with Microsoft | openid, profile, email, offline_access, User.Read | Delegated | Sign users in and read their own profile |
| Person picker & prefill | User.Read.All | Application | Search the directory and read job title, department and manager |
| Groups | GroupMember.Read.All | Application | "Specific groups" form access and mapping the admin group |
| Mail.Send | Application | Send notifications from a mailbox. Restrict this with an application access policy. | |
| SharePoint / Excel | Sites.ReadWrite.All, Sites.Manage.All | Application | Save responses to SharePoint lists (Manage: create lists and columns) and files |
| SharePoint / Excel | Files.ReadWrite.All | Delegated | Excel rows, written as the connected Excel account (see below) |
Excel: the connected account
Microsoft's Excel (workbook) API only works for a signed-in account — it has no application permissions — so, like a Power Automate connection, an administrator connects one account under Administration → Microsoft 365 → Excel account → Connect Excel account (a normal Microsoft sign-in through your own SnagBane app registration). Use a service account (for example forms-bot@company.com) and give it Can edit on the workbooks you use. SnagBane keeps its refresh token encrypted; if the sign-in expires or is revoked (password reset, the account is disabled), the Excel integration says so and you connect again.
In a form's Integrations tab, add Excel, paste the workbook's sharing link (Share → Copy link) and a table name. If the table doesn't exist SnagBane creates it on a new sheet with a column per question, adds missing columns later, and writes one row per response. Answers that start like a formula (=, +, -, @) are written as text.
Sign in with Microsoft
Once connected, the login page shows Sign in with Microsoft. With Create accounts automatically on, anyone in your tenant gets an account with the default role when they first sign in. Pick an Admin group and its members become SnagBane admins each time they sign in.
Connect Google Workspace
Go to Administration → Google Workspace and tick the features you need. Google does not allow apps to create OAuth clients for you, so the page gives you a short checklist with every value ready to copy:
- Sign in with Google: create an OAuth client (Web application) with the origin and redirect URI shown, paste its ID and secret. Set “Restrict to domain” to your Workspace domain.
- Directory person picker & prefill: create a service account, upload its JSON key, then in the Workspace Admin console add domain-wide delegation for its client ID with the read-only directory scopes shown. Enter an admin email to impersonate.
- Gmail (optional): add the
gmail.sendscope and choose “Google Workspace (Gmail API)” in Admin → Email. - Google Sheets: just share each spreadsheet with the service account's email.
Click Save & test — each part is checked and any problem is explained. If Microsoft 365 is also connected, choose which directory person pickers use under Admin → Security.
Sign in with Okta
Administration → Okta. In the Okta Admin Console create an app integration (OIDC – OpenID Connect, Web Application, grant type Authorization Code) with the sign-in redirect URI shown on the page, assign it to your people, then paste the Okta domain (e.g. company.okta.com), client ID and secret and click Save & test. Leave Authorization server empty to use the org server, or enter e.g. default for a custom one.
- Groups: in the Okta app's Sign On tab set the Groups claim (name
groups, filter Matches regex.*or narrower). Then Administration → Groups → New group → Okta and type the Okta group name (names seen at sign-in are suggested). Add the group to workspaces and/or give it an organisation role — members get it automatically. - Okta sends groups when people sign in, so group changes apply at their next Okta sign-in (Microsoft 365 and Google groups are checked live).
- Person pickers and profile prefill keep using Microsoft 365 or Google Workspace.
- Job title, department and manager (optional): add custom claims named
title,departmentandmanager(a name or an email address; ormanager_email) to the ID token, each with an Okta Expression Language expression for the matching attribute of the Okta user profile (see the attribute names under Directory → Profile Editor). How to add claims: Okta's guide Customize tokens returned from Okta with custom claims. SnagBane reads them at each sign-in; claims that are only in/userinfowork too.
Job title, department & manager
Every account has a job title, department and manager. They are shown in Administration → Users & roles (search finds them too), used for Manager approval steps and to prefill forms.
- Microsoft 365 / Google Workspace: filled from the directory — at sign-in and after each directory sync (every night, or Sync now on the Microsoft 365 / Google Workspace page). Uses the permissions the directory features already have.
- Okta: from the claims described under Sign in with Okta.
- By hand: Users & roles → ⋯ → Edit details. What you type is kept: the directory no longer changes these fields for that person until you tick Update these fields from the directory again.
- Without any directory (Community), Manager approval steps and prefill use what you typed here.
Sign-in methods & passwords
The sign-in page always offers email + password, plus a button for each single sign-on that is set up and switched on: Microsoft, Google, Okta. A person is linked to a provider the first time they sign in with it (matched by email).
- Administration → Security → Allow passwords for Microsoft, Google and Okta accounts (on by default). Turn it off to make linked people use their single sign-on; owners can still use a password so you can't lock yourself out if single sign-on breaks.
- Only owners can give a password to a linked account.
- With fast person search on, an account that is disabled or deleted in Entra ID (or suspended in your Google Workspace domain) can no longer sign in with a password either — from the next directory sync.
- SnagBane never changes anything in Entra ID, Google or Okta — its access there is read-only; SnagBane passwords exist only in SnagBane.
Paste your questions (fastest start)
Click New form → Paste your questions and paste a list — from Word, an email or an old paper form. One question per line; SnagBane picks the type from the wording (email, date, number, rating, person, file, long answer…) and you can change any type in the preview before creating the form.
- Lines starting with
-under a question become its choices (2 choices “Yes/No” become a yes/no question, more than 6 a dropdown); use[ ]for “pick several”. - End a question with
*to make it required. A line---starts a new page; a first line starting with#becomes the form title. - Force a type with a hint at the end:
[date],[long],[email],[number],[dropdown],[multiple],[person],[file],[stars].
Form builder
Click a question type on the left (or drag it onto the canvas), then edit it in the right-hand panel. Drag the handle to reorder, or hover a question and click the round + below it to insert a new one right there. Everything saves automatically. Turn on Live preview (wide screens) to try the form on desktop or mobile while you build — logic, branching and validation work, nothing is saved. Less-used options (show-if conditions, placeholders, length limits, prefill, variable names) are under Show advanced options in the question panel. If two people (or two browser tabs) edit the same form, nothing is overwritten silently: the later save stops and asks whether to load the newest version or keep yours. The available types are short and long answer, email, number, phone, website, single choice, multiple choice, dropdown, yes/no, star rating, scale/NPS, date, time, file upload, hidden field, person picker, heading, text block and page break.
Person picker & Microsoft 365 prefill
A Person picker question searches your tenant as the respondent types. It matches display name, email and UPN, and shows job title and department. Turn on Allow multiple people to collect several.
Prefill from Microsoft 365 fills a question from the signed-in respondent's Entra ID profile. You can use the respondent, their manager, display name, email, job title, department, office, mobile or employee ID. Respondents can still change the value.
Fast person search. SnagBane keeps a small local copy of the directory (enabled users only: name, email, job title, department, office, manager), so the picker answers in milliseconds even in tenants with tens of thousands of people and never hits Microsoft's rate limits. It refreshes every night (02:30) and catches up hourly if a run was missed; Administration → Microsoft 365 → Fast person search shows the status and has Sync now. From the command line: php artisan snagbane:directory-sync. Until the first sync finishes, searches go live to Microsoft Graph / Google as before. Needs the scheduler (cron * * * * * php artisan schedule:run) — already running in the Docker stack.
Conditional logic
Enable Conditional logic on any question to show it only when earlier answers match. For example, show "Business justification" when "Estimated cost" is greater than 500. Hidden questions are never validated or stored, and the rule is enforced again on the server.
Branching (side-by-side columns)
Give each answer its own questions. Select a single-choice, dropdown or yes/no question and click Add branching. Lines go out from the question into columns side by side, one per answer, like a switch in Power Automate, and join again below: the questions under the columns are the shared part everyone answers. You see the whole logic at once, without scrolling up and down.
- Questions in a column — click + Add question in a column, drag a question (or a type from the left) into it, or right-click a question and pick Move to column….
- Names — a column is named after its answers (“Unpaid, Other”) and follows them when you change the answers. Double-click the name (or right-click → Rename) to give it your own name.
- Several answers, one column — drag a column's header onto another column to put their answers together; right-click → Split into one column per answer separates them again.
- End of a column — the button at the bottom of each column switches between Continue below (the shared questions) and End the form. A column without questions sends that answer straight on.
- Same page or its own section — a column's questions appear right under the answer, on the same page. For longer branches, right-click the column → Open as its own section: respondents then get a page for it, titled with the column's name. There is no section to manage for the join; your own sections (New section / page) stay separate from branching.
- Right-click a question or a column for its actions: add a question, rename, move left/right, join or split columns, move a question into a column or below the columns, remove branching.
- Remove branching keeps the questions of every column, one after another.
Example: “Leave type” — Annual leave (no extra questions), Sick leave → “From” and “To”, Unpaid, Other → End the form; then everyone else answers “Who covers for you?” and “Manager”.
An unanswered branching question (optional, or hidden by a condition) skips all columns. Answers from columns the respondent didn't take are never saved. Forms made before columns existed keep their “go to” menus and keep working; when the branching means the same thing as columns (at most one later target), the question card offers Show as columns. Columns made with 2.10 stay sections of their own. Branching inside a column is not available yet.
For more complex forms:
- Sections — add a New section / page break to split a form into pages with their own title; answers can also jump to a section.
- After this section — on a section break, set where respondents go next by default, plus routing rules with several conditions (all / any, numbers, text, empty/answered).
- Show only if… (under Show advanced options) — show a question only when earlier answers match, e.g. several conditions combined.
- The Logic overview tab lists every branch, rule and conditional question in plain language.
Answers to skipped questions and sections the respondent never visited are discarded, and the server recalculates the path, so hidden questions can't be forced in.
Calculations & answer piping
Add a Calculation question and write a formula such as {qty} * {price} or SUM({items.amount}). Supported: + - * / %, comparisons, AND/OR, SUM AVG MIN MAX ROUND FLOOR CEIL ABS COUNT IF CONCAT DAYS TODAY. Give choice options a numeric value to score them. Totals update live for respondents and are always recomputed on the server.
Piping: put {{variable_name}} in any question, description, confirmation or receipt message to insert an earlier answer.
Repeating tables, grids & signatures
- Repeating table — line items (expenses, attendees, equipment) with text, number, date and choice columns, min/max rows and live column totals.
- Grid (matrix) — rate several rows on the same scale, one or several answers per row.
- Signature — draw with mouse, finger or pen; stored as a private PNG and embedded in printouts.
- Validation — min/max length, formats (letters, digits, postcode, IBAN or your own pattern), whole numbers, date ranges and min/max selections.
Quiz mode
In Settings → Quiz mode turn on grading, then mark correct answers (✓ next to options, or accepted text answers) and points per question. Respondents can see their score immediately; results show the average score, a distribution and the % correct per question. Answer keys are never sent to the browser.
Approval workflows
Open a form's Approvals tab and add stages. Each stage's approver can be the person chosen in a question, the respondent's manager from Microsoft 365 / Google Workspace, specific people, or email addresses. Choose whether any one or everyone must approve, add reminders and optional conditions (e.g. only send to Finance when the total is over 500).
Approvers get a branded email with Review & approve and Reject buttons (single-use, secure links) and also see requests under Approvals in SnagBane. Uploaded files and signatures of the request open straight from the approval page or the inbox, even for approvers who have no access to the form itself. The requester is emailed the outcome, and every step is recorded. If a form lets respondents edit after submitting, a rejected request can be corrected with the edit link and then goes through approval again from the first stage. Events approval.requested, approval.approved and approval.rejected can trigger integrations and webhooks.
How approvals work, step by step
- Someone submits the form → the response gets status pending and the first stage starts (stages whose condition doesn't match are skipped).
- The stage's approver(s) get an email with single-use Approve/Reject links and see it in Approvals. With “any” one decision completes the stage; with “all” everyone must approve. Rejecting requires a comment.
- Approved → next stage; after the last stage the response is approved. Any rejection ends it as rejected. The requester is emailed the outcome and sees it live under My requests on a portal.
- Change approver: when an approver is away or doesn't answer, an administrator (or the owner of the form's workspace) hands the waiting approval to someone else — Dashboard → Waiting longest / Overdue approvals, or the request under Results → Approvals. The new approver is notified right away, the old link stops working, and the audit log keeps who changed it, from whom to whom and the optional note. The requester and deactivated accounts can't be picked.
- Overdue approvers get reminders (due days per stage). If an approver can't be resolved (no manager in the directory, empty person question) the task goes to the form owner, so nothing is auto-approved by mistake.
- The requester can't approve their own request (a switch per stage, on for new stages): if the requester picks themselves, or is one of the approvers, the task goes to the other approvers; if nobody is left, to the requester's manager, else the form owner, or another administrator when the owner is the requester.
- Each step fires events for Power Automate, Teams, Slack and webhooks — e.g. create the laptop order only after
approval.approved.
Phone app (install SnagBane) — Enterprise
SnagBane can be installed as an app on phones, tablets and computers — straight from your own SnagBane site, no app store and nothing sent to us. The app opens without the browser bars, shows your organisation's name and colour, and can notify approvers when a request is waiting for them.
- Open SnagBane in the browser on the phone and sign in. Under Approvals you see Approvals on your phone.
- Install: tap Install app where the browser offers it (Chrome, Edge on Android and desktop). On iPhone and iPad, add SnagBane to the Home Screen from Safari and open it from there.
- Notifications: tap Turn on notifications and allow them. Each device is turned on separately; turn it off with the same button.
- Requirements: the site must use
https://(browsers only allow installable apps and notifications on secure sites), and the SnagBane server must be able to reach the browser's push service on the internet over HTTPS (Google for Chrome and Android, Apple for Safari, Mozilla for Firefox, Microsoft for Edge). On iPhone and iPad, notifications work in the app added to the Home Screen (iOS / iPadOS 16.4 or later). - Privacy: a push carries no content. The phone is only told “something is new”; the app then asks your SnagBane server what is waiting and shows it. Form titles, names and answers never pass through Google, Apple, Mozilla or Microsoft.
- The notification opens Approvals, where the request can be approved with one tap. The app icon shows how many requests are waiting, where the phone supports it.
Workspaces, roles & groups
Workspaces are spaces for teams or departments. Every form, portal and workspace-wide integration belongs to one workspace; members see only their workspaces. Use the switcher at the top of the sidebar to work in one workspace or see all. Create one under Workspaces → New workspace (name, colour, icon, then members).
- Workspace roles: Owner — members, settings, workspace integrations and the approvals overview (with Reassign when an approver is away); Editor — creates forms and portals; Viewer — sees forms and results.
- Organisation roles (Users & roles): Owner — everything; Administrator — all workspaces and organisation settings, but not security, Microsoft 365 / Google connections, licence or system; User & access admin — editor plus users, groups and workspace members; Editor; Viewer. The full matrix is under “What each role can do”.
- Groups (Administration → Groups): manual, or linked to an Entra ID / Google group (membership checked live). Add groups to workspaces, and optionally give every member an organisation role (e.g. group “IT admins” → Administrator).
- Organization settings decide who may create workspaces and whether new users join a workspace automatically (off by default; when on, you pick the workspace and their role there — Viewer by default).
Portals
A portal is one page that lists several forms — like an HR or IT service desk. Open Portals → New portal, pick a look, tick the forms to include and click Create portal. There are two looks (switch any time under Design → Look):
- Modern — a calm company service desk: a top bar with your logo, a greeting for signed-in colleagues (“Good morning, Ana”), their latest requests with the approval steps (“Step 1 of 2 · Manager”) and the services as a tidy list, grouped by category, with the time to fill in and who approves (“2 min · Manager → Finance”). From three categories up, a list on the left jumps between them.
- Classic — a colourful banner with the forms as cards, a list or tiles.
In the editor:
- Content: group forms into categories, reorder them, add links (intranet, handbook), and give each item its own title, description, icon, colour or a bigger “highlight” card. Leave the title empty to use the form's own (translated) title.
- Design: the look (Modern or Classic); for Classic: cards, list or tiles; cards per row; banner, simple or no header; accent, background and header colours; upload a logo and cover (or pick a ready-made cover); card style and corners. The preview on the right updates live (desktop/mobile).
- Access & options: anyone with the link, your organisation, or specific people/groups. Each form keeps its own access rules: people only see forms they can fill in (or, if you prefer, greyed out with a sign-in hint). Toggle search, descriptions and the My requests tab, add footer text and choose the web address (
/p/hr). - Languages: offer the portal in other languages. Pick the language you write the portal in, add a language and translate the title, welcome text, footer, categories, links and every block (or Translate missing automatically with DeepL, Azure AI Translator, Google or LibreTranslate; CSV export/import for translators). Visitors see the portal in their language (language picker, their profile or their browser); texts that are not translated yet appear in the main language. Forms on the portal use their own translations. Preview shows a translation in the live preview; texts that changed after they were translated are marked for review.
Company portal (Enterprise, administrators): at the top of Portals, click Set up. You get one page for the whole company, in the Modern look: every workspace is a department (IT, HR, Finance…) and its published forms appear by themselves — no picking forms by hand. Everyone sees only the forms they may open, and My requests covers every department. Under Content → Departments hide a workspace or change the order; notices, FAQ and contacts you add appear under the departments. Visitors can pin their own department (the pin next to it) so it opens first for them. Publish it and add it to Microsoft Teams as the company tab (Microsoft Teams in the editor). In the Community edition the card shows what the Enterprise edition adds.
Press Publish and share the link or put it on your intranet/Teams tab. Editors see and manage their own portals; admins see all. Draft portals and draft forms are only visible to their editors.
Page builder: in the portal editor's Content tab, add blocks with + or Add block — form categories, text (supports **bold**, *italic*, [links](https://…) and “- ” lists), images, buttons, info cards, FAQ, notices, contacts and dividers. Reorder, duplicate, and give each block a background.
Own domains for portals
Administration → Domains → add e.g. hr.company.com and choose the portal. Then connect it:
- Shared hosting (cPanel, Plesk…): create the subdomain and set its document root to the same
publicfolder as SnagBane; enable AutoSSL / Let's Encrypt. - VPS / Docker: point DNS at the server (CNAME or A record) and add the host to your web server or reverse proxy with a certificate.
- Click Check. SnagBane confirms the domain reaches this installation (or a TXT record
_snagbane.<domain>with the shown token). Sign-in with Microsoft/Google happens on the main address (APP_URL); people then return to the portal on its own domain, already signed in (a single-use hand-over code, valid for one minute and only for that verified domain).
Links SnagBane sends out (approval emails, notifications, webhooks) always use APP_URL, or the custom domain the person is on — never an address taken from an unverified request, so they can't be spoofed.
Editions & licence
Community is free and complete on its own — for organisations that don't use Microsoft 365 or Google Workspace, or simply don't need them. Enterprise connects SnagBane to Microsoft 365, Google Workspace and Okta, and adds the phone app, the REST API, Power Automate / Zapier and the premium connectors. In Community these show with an Enterprise badge, so you can see what you would get.
| Community (free) | Enterprise (licence) | |
|---|---|---|
| Forms, builder, logic, calculations, quizzes, files, signatures, translations | ✓ | ✓ |
| Workspaces, roles, manual groups, approvals (all stages), portals, own domains | ✓ | ✓ |
| Sign-in | SnagBane accounts, two-step verification | + Microsoft, Google and Okta sign-in |
| Person picker & prefill | from SnagBane users | from Entra ID / Google Workspace, with fast local directory copy |
| Groups from Entra ID / Google / Okta | — | ✓ |
| Portals as Microsoft Teams apps (Teams sign-in) | website tab only | ✓ |
| SMTP, SendGrid, Mailgun, Postmark, Brevo, Resend, Amazon SES | + Microsoft 365 mailbox, Gmail | |
| Integrations | Make, n8n, signed webhooks, HTTP request, Discord, Telegram, Mattermost, Dropbox, Notion, Airtable, CRM & newsletter tools, email, local export | + Microsoft Teams, Slack, Amazon S3 / R2 / Wasabi / MinIO, SharePoint, Excel, Google Sheets, Drive, Chat |
| Power Automate & Zapier automations, Power Automate custom connector | — | ✓ |
| REST API & personal API tokens | — | ✓ |
| Phone app with notifications for approvers | — | ✓ |
| Company portal (every workspace as a department, for a Teams tab) | — | ✓ |
| White-label (hide “Powered by SnagBane”) | — | ✓ |
Community download → Enterprise: the Community download does not contain the Enterprise module. Activate your licence (below), then Administration → System → Updates → Get SnagBane Enterprise installs it over your copy; your data and settings stay.
Activate: Administration → License, paste the key from your purchase email or your Freemius account, click Activate. The server needs outgoing HTTPS to api.freemius.com and to the SnagBane licence and update service snagbane-updates.markostepanovic.workers.dev; SnagBane re-checks the licence once a day. Deactivate on this server frees the licence so you can use it on another server.
- No grace period: when the licence ends (expired or cancelled), Enterprise pauses right away. If the licence service can't be reached, Enterprise keeps working for at most 2 days while SnagBane retries every hour — Administration → License shows when it would pause.
- While paused, forms, responses, approvals, workspaces and groups keep working. Okta and Teams sign-in, own domains, fast person search and premium integrations pause, and Enterprise settings can't be changed until you renew. Nothing is deleted: your data and settings stay on your server, and once the licence is active again (renew, or activate a new key) everything continues where it was.
- Only a licence unlocks Enterprise: the licence service signs a proof of your Freemius licence that SnagBane checks itself; settings, the database or the server address can't turn Enterprise on.
Portals in Microsoft Teams
Open a portal in the editor and click Microsoft Teams — the same two-option guide as below is built in, with your links pre-filled. Both options need SnagBane on a public HTTPS address (Teams never loads http://). For a quick test on your PC, a tunnel such as Cloudflare Tunnel or ngrok gives you an https address; set it as the site URL.
Option 1 — Teams app (recommended)
The portal becomes an app with its own icon in the Teams app bar, can be added to any channel or chat, and people are signed in automatically with their Teams account (“My requests”, organisation-only forms and manager prefill all work).
- Download the app package: portal editor → Microsoft Teams → Teams app → Download Teams app (.zip). The zip holds the manifest (name, icon in the portal colour, address) — nothing to edit.
- Upload it: a Teams administrator opens admin.teams.microsoft.com → Teams apps → Manage apps → Upload new app and selects the zip.
- Make it available: check that the app is Allowed. To pin it for everyone: Teams apps → Setup policies → Global → Pinned apps → Add apps.
- Use it: people open it from Apps (or the pinned icon). To add it to a channel: open the channel → + → search for the app → Save.
- Just testing? Without admin rights: Teams → Apps → Manage your apps → Upload an app → Upload a custom app (if your organisation allows custom apps).
Automatic sign-in (Teams single sign-on) — Admin → Microsoft 365 → Microsoft Teams sign-in → Enable Teams sign-in. A Global Administrator signs in once more with Sign in with Microsoft (the same copy-the-address steps as the setup); SnagBane then adds “Expose an API” (api://<your-host>/<client-id>, scope access_as_user, pre-authorised for Teams, Microsoft 365 and Outlook) to its existing app registration. New installations on HTTPS get this automatically during the Microsoft 365 setup. Download the package again afterwards (it then contains the single sign-on block) and choose Update in Teams Admin Center. Without single sign-on the app still works: people click Sign in once and sign in in a pop-up (Microsoft, Google or email).
Manual / PowerShell setups: in Entra ID → App registrations → SnagBane Forms → Expose an API, set the Application ID URI to api://<your-host>/<client-id>, add a scope access_as_user (admins and users), and add the client applications 1fec8e78-bce4-4aaf-ab1b-5451cc387264 (Teams desktop/mobile) and 5e3ce6c0-2b1f-4285-8d4b-75ee78787346 (Teams web) for that scope. Then run Enable Teams sign-in once or set it on in Admin → Microsoft 365.
Updates: changes to forms, categories and design appear in Teams immediately. Only a new portal name or colour needs a new package (Update in Teams Admin Center).
Option 2 — Website tab (no admin needed)
- Portal editor → Microsoft Teams → Website tab → Copy the tab link (it ends in
?teams=1). - In Teams, open the channel → + (Add a tab) → Website → give the tab a name, paste the link → Save.
- The portal is a tab for everyone in the channel; forms open inside it with a Back to the portal link.
Limits: Teams doesn't allow signing in inside a website tab, so use it for portals set to Anyone with the link. “My requests” and organisation-only forms need the Teams app (Option 1).
Technical notes
- Portals (
/p/…) may be framed by Teams / Microsoft 365 / Outlook hosts only. Add more hosts (e.g. your SharePoint) withSNAGBANE_PORTAL_FRAME_ANCESTORS="https://contoso.sharepoint.com". Public forms (/f/…) stay embeddable anywhere (SNAGBANE_FRAME_ANCESTORS). - On HTTPS, session cookies are sent as
SameSite=None; Secure; Partitionedso sign-in works inside Teams (CSRF tokens still protect every request). SetSNAGBANE_EMBED_COOKIES=falseto keepSameSite=Laxif you don't use Teams. - Teams single sign-on tokens are verified with Microsoft's signing keys (signature, tenant, audience, expiry,
access_as_userscope) before a session is created; the pop-up fallback hands over a single-use code valid for two minutes.
Save & resume, editing and receipts
- Save & finish later gives respondents a private link valid for 30 days.
- Let respondents edit after submitting adds an edit link to the thank-you page and receipt. With approvals, a request can be edited after it was rejected (it then goes back to approval), not while it waits or once it is approved.
- Receipts show the answers in the language the respondent used; hidden fields are left out. Receipts to an address typed into a form (not a signed-in account) are limited to 10 per hour per address, so a public form can't be used to send email to anyone.
- Every respondent gets a private receipt page showing their answers and approval status, printable to PDF.
- Admins can print any response (with signatures and approval history) and export all responses to Excel, CSV or JSON.
Access control
| Mode | Who can respond |
|---|---|
| Anyone with the link | Public, no sign-in |
| My organisation | Anyone who signs in with your Microsoft 365 tenant |
| Specific people & groups | Selected users, or members of selected Entra ID groups (nested groups included) |
You can also allow one response per person, make responses anonymous, set open and close dates, and cap the number of responses. Open and close times are in the organisation's timezone (Administration → General).
People who may not open a form (not signed in, not on the list, form closed or not published) see only its title and description — the questions are not sent to their browser.
Responses & export
The Results page has a summary (charts per question, NPS score, 30-day trend) and a searchable table. Click a row to see the full response with the respondent's department and manager. Export CSV produces an Excel-friendly UTF-8 file.
Languages & multilingual forms
SnagBane ships in 33 languages: English, Español, Français, Deutsch, Italiano, Português (Brasil), Português (Portugal), Nederlands, Polski, Čeština, Slovenčina, Magyar, Română, Български, Ελληνικά, Srpski (latinica), Српски (ћирилица), Hrvatski, Slovenščina, Українська, Русский, Türkçe, Svenska, Dansk, Norsk bokmål, Suomi, العربية (right-to-left), हिन्दी, Bahasa Indonesia, 日本語, 한국어, 简体中文 and 繁體中文. Emails, validation messages, dates and numbers follow the language too.
- Admin → Languages: choose the default language, which languages people may pick, and whether the visitor's browser language is detected automatically. Every user can pick their own language in the sidebar or in Your profile; guests can switch on the sign-in page.
- Customise wording (same page): search any text in any language and replace it — e.g. “Submit” → “Send request”. Overrides are stored in the database and survive updates.
- Multilingual forms: in the builder open the Languages tab, set the form's main language and add translations. Every question, option, section, help text, confirmation message and button can be translated side by side. Respondents get their browser language automatically (or
?lang=dein the link) and can switch with the language picker. Answers are stored by option id, so results, charts and exports stay in one place; exports get a Language column. - Automatic translation (optional): connect DeepL, Azure AI Translator, Google Cloud Translation or a self-hosted LibreTranslate in Admin → Languages, then click “Translate missing automatically” in the form's Languages tab. Merge tags such as
{{name}}are protected. Changed source texts are flagged “needs review”. - Professional translators: export a form's texts as CSV, send them out, and import the file back.
- Adding another interface language: copy
lang/en.jsontolang/<code>.json, translate the values (keys stay English), add the language toconfig/locales.phpand runnpm run build.php scripts/i18n-extract.php --checkreports missing texts and broken placeholders; seescripts/TRANSLATOR_GUIDE.md.
Branding (white-label)
In Administration → Branding & general you can set the site name, logo, favicon, primary and accent colours, login headline and footer text, and hide "Powered by SnagBane". Each form also has its own theme: accent colour, background, logo and cover image.
Integration catalog
Open a form's Integrations tab (or Integrations & API for all forms), pick a connector, fill in the settings and click Send test. Every integration can run on new, edited, approved or rejected responses, and only when your conditions match (e.g. post to Telegram only when Priority is Urgent). Settings support merge tags like {{respondent.name}}, {{all_answers}} or any {{variable_name}}. Secrets are stored encrypted and never shown again; each run is logged with its result.
| Category | Connectors |
|---|---|
| Chat & notifications | Microsoft Teams, Slack, Telegram, Discord, Google Chat, Mattermost / Rocket.Chat |
| Email & marketing | Send email (custom, branded, with attachments), Mailchimp, Brevo, MailerLite, Kit (ConvertKit), ActiveCampaign |
| Spreadsheets & databases | Google Sheets, Excel tables (auto-creates the table and columns), SharePoint lists (auto-creates the list and columns), Airtable, Notion |
| Files & storage | Dropbox, Google Drive, SharePoint / OneDrive, Amazon S3 / Cloudflare R2 / Wasabi / MinIO |
| CRM & custom | HubSpot, Custom HTTP request (any REST API) |
| SMS | Twilio SMS / WhatsApp |
| Local server | Local folder export (CSV + JSON lines + per-response folders with files), Local log file (JSON lines for SIEM) |
| Automation | Power Automate, Zapier, Make, n8n and signed webhooks (see below) |
Setup guides
The same steps are shown in SnagBane above each integration's settings, in the user's language, with a link to the provider's official guide. Menu names are the provider's own (in English).
ActiveCampaign
- In ActiveCampaign, click Settings (gear icon) → Developer.
- Copy the API URL and the API key into the fields below.
- Optional: the numeric ID of the list to subscribe contacts to.
Official guide: help.activecampaign.com
Airtable
- Open airtable.com/create/tokens (Developer hub → Personal access tokens) and create a token.
- Add the scope data.records:write.
- Under Access, add the base to write to, create the token and copy it (it is shown only once).
- Base ID: the part starting with “app” in the base’s address (airtable.com/app…/…).
- Table: the table name exactly as in Airtable, or its ID (tbl…).
Official guide: airtable.com
Amazon S3 / R2 / Wasabi / MinIO
- AWS: create the bucket, then IAM → Users → Create user (no console access needed).
- Attach a policy that allows s3:PutObject on arn:aws:s3:::your-bucket/*.
- Open the user → Security credentials → Create access key (“Application running outside AWS”) and copy both keys.
- Leave Endpoint empty and enter the bucket’s region.
- Cloudflare R2: R2 → Manage API tokens → Create API token with Object Read & Write for the bucket. Endpoint: https://<ACCOUNT_ID>.r2.cloudflarestorage.com, region: auto.
- MinIO, Wasabi and others: use their S3 endpoint and access keys.
Official guide: docs.aws.amazon.com
Brevo contacts
- In Brevo, open the account menu → Settings → SMTP & API → API Keys & MCP.
- Click Generate a new API key, name it, choose an expiry and copy it (it is shown only once).
- Optional: the IDs of the lists to add contacts to (Contacts → Lists), separated by commas.
Official guide: help.brevo.com
Custom HTTP request
- Take the URL and method from the service’s API documentation.
- Add headers such as Authorization: Bearer … (stored encrypted).
- Write the JSON body with merge tags; values are escaped automatically.
- Save and click Send test to see the reply.
Discord
- In Discord, open Server Settings → Integrations (you need the Manage Webhooks permission).
- Click Create Webhook (or Webhooks → New Webhook), give it a name and choose the channel.
- Click Copy Webhook URL and paste it below.
Official guide: support.discord.com
Dropbox
- Open dropbox.com/developers/apps → Create app → Scoped access → App folder (or Full Dropbox).
- On the Permissions tab tick files.content.write and files.content.read, then click Submit.
- Copy the App key and App secret from the Settings tab into the fields below and save.
- Click Get authorisation code, allow access, paste the code and click Connect.
Official guide: www.dropbox.com
Excel (Enterprise)
- In Administration → Microsoft 365, turn on “Save responses to SharePoint / Excel” and connect an Excel account (Microsoft’s Excel API only works as a user).
- Open the workbook in Excel or SharePoint → Share → Copy link, and paste it below. The connected account must be able to edit it.
- Enter a table name. With automatic creation on, the table and missing columns are created for you.
- Save and click Send test.
Google Chat (Enterprise)
- Open the space in Google Chat in a browser (webhooks can’t be added in the mobile app).
- Click the arrow next to the space name → Apps & integrations → Add webhooks.
- Enter a name and click Save, then click More (⋮) → Copy link and paste it below.
- Option missing? Your Google Workspace admin has to allow incoming webhooks.
Official guide: developers.google.com
Google Drive (Enterprise)
- Set up the service account once in Administration → Google Workspace, with the Google Drive API enabled.
- Service accounts have no storage of their own: open a Shared Drive → Manage members and add the service account email as Content manager.
- Open the folder in that Shared Drive and paste its link below.
- Or fill in “Upload as user (domain-wide delegation)” to save into that person’s My Drive.
Official guide: support.google.com
Google Sheets (Enterprise)
- Set up the service account once in Administration → Google Workspace, with the Google Sheets API enabled.
- Open the spreadsheet → Share and add the service account email as Editor.
- Paste the spreadsheet link below. The tab is added if it doesn’t exist.
Official guide: developers.google.com
HubSpot
- In HubSpot, open Development → Keys → Service keys (or Settings → Integrations → Service keys) and create a key. You need Developer tools access.
- Name it and add the scope crm.objects.contacts.write.
- Copy the key and paste it below. A token from an existing legacy private app works too.
- Map answers to HubSpot contact properties below.
Official guide: developers.hubspot.com
Kit (ConvertKit)
- In Kit, open Settings → Developer and add a new API key (v4).
- Copy the key (it is shown only once) and paste it below.
- Optional tag ID: open the tag under Grow → Subscribers; the number in the page address is its ID.
Official guide: help.kit.com
Local folder export
- Files are written on the server below the export root shown under Sub-folder.
- With Docker, mount that folder as a volume or network share to reach the files.
- Choose what to write: responses.csv, responses.jsonl and/or a folder per response.
Local log file
- Each event is appended as one JSON line to storage/logs/forms/<form>.log.
- Point your log shipper (Filebeat, Fluent Bit, Vector, Splunk forwarder…) at storage/logs/forms/*.log.
- Turn off the Include answers option for privacy-sensitive forms.
Mailchimp
- In Mailchimp, click your profile icon → Profile → Extras → API keys.
- Click Create A Key, name it, click Generate Key and copy it (it is shown only once).
- Audience ID: Audience → Settings → Audience name and defaults → Audience ID.
Official guide: mailchimp.com
MailerLite
- In MailerLite, open Integrations → MailerLite API → Use.
- Click Generate new token, name it and copy it right away (it is shown only once).
- Group IDs are listed in the Groups section of the same page; separate several with commas.
Official guide: www.mailerlite.com
Mattermost / Rocket.Chat
- In Mattermost, open the Product menu → Integrations → Incoming Webhooks → Add Incoming Webhook.
- Enter a name, choose the channel and click Save.
- Copy the URL (https://your-server/hooks/…) and paste it below.
- No Integrations menu? A System Admin enables incoming webhooks in System Console → Integrations → Integration Management.
Official guide: developers.mattermost.com
Microsoft Teams (Enterprise)
- In Teams, click ⋯ next to the channel or chat and choose Workflows.
- Pick “Send webhook alerts to a channel” (or “Send webhook alerts to a chat”).
- Check the team and channel, click Save and copy the webhook link.
- Paste it below, save, then click Send test.
Official guide: support.microsoft.com
Notion
- In Notion, open Settings → Developer and turn on developer features (workspace owners only).
- Click + New connection, give it a name and choose the workspace.
- Click ••• next to the connection, copy its token and paste it below.
- Give the connection access to your database: its Content access tab, or the database’s ••• menu → Connections.
- Paste the database link below. Property types are detected automatically.
Official guide: developers.notion.com
Send email
- Email is sent through the service set up in Administration → Email.
- Enter recipients separated by commas; a merge tag such as {{email}} sends to an address from the form.
- Add conditions below to route by answer, e.g. a different team per department.
SharePoint / OneDrive files (Enterprise)
- In Administration → Microsoft 365, turn on “Save responses to SharePoint / Excel” and grant admin consent.
- Enter the site address; files are saved in the site’s Documents library.
- Choose a folder; each response gets its own sub-folder.
- Save and click Send test to check the permissions.
SharePoint list (Enterprise)
- In Administration → Microsoft 365, turn on “Save responses to SharePoint / Excel” and grant admin consent.
- Enter the site address, e.g. https://contoso.sharepoint.com/sites/HR.
- Choose a list name. With automatic creation on, the list and its columns are created for you.
- Save and click Send test to check the permissions.
Slack
- Go to api.slack.com/apps and create an app (From scratch) in your workspace.
- Open Incoming Webhooks and turn on Activate Incoming Webhooks.
- Click Add New Webhook, pick the channel and authorise it.
- Copy the webhook URL (https://hooks.slack.com/services/…) and paste it below.
Official guide: docs.slack.dev
Telegram
- In Telegram, message @BotFather and send /newbot.
- Choose a name and a username ending in “bot”, then copy the token BotFather sends you.
- Add the bot to your group, or as an administrator of your channel.
- Chat ID: @channelname for a public channel. For a group, send /start in it, open https://api.telegram.org/bot<token>/getUpdates and copy chat → id (a negative number).
Official guide: core.telegram.org
Twilio SMS / WhatsApp
- Sign in to the Twilio Console (console.twilio.com). The Account SID and Auth token are under Account Info.
- From: a Twilio phone number you own (+1555…), or whatsapp:+… for a WhatsApp sender.
- To try WhatsApp: Console → Messaging → Try WhatsApp, and join the sandbox from your phone first.
- To: a number in international format, or a merge tag such as {{phone}}.
Official guide: help.twilio.com
Email providers
Admin → Email supports SMTP, Microsoft 365 (Graph), Google Workspace (Gmail API), SendGrid, Mailgun (US/EU), Postmark, Brevo, Resend and Amazon SES — all via their official HTTPS APIs, no extra software. Use Log only while testing, or Mailpit in the Docker stack.
Power Automate (Enterprise)
Option A: HTTP trigger (2 minutes)
- Create a flow with the trigger When an HTTP request is received.
- In SnagBane, open the form, go to Automations and click Copy JSON schema. Paste the schema into the trigger's Request Body JSON Schema.
- Save the flow and copy its URL. Back in SnagBane, click Power Automate, paste the URL and click Test.
Your answers are available as response.fields.<variable_name>, and the respondent as response.respondent.name/email/department/manager.
Option B: Custom connector (recommended for organisations)
- Download snagbane.swagger.json, which is also available under Automations & API.
- In Power Automate, go to Data → Custom connectors → New custom connector → Import an OpenAPI file.
- Change Host to your SnagBane domain. Under Security, choose API Key, set the parameter label to "API token", the name to
Authorizationand the location to Header. - Create a connection and enter
Bearer sb_…(from Automations & API → New token).
Flow makers now get the trigger When a response is submitted (with a form dropdown) and the actions List forms, Get responses and Get a response. SnagBane registers the flow's callback URL automatically and removes it when the flow is deleted.
Zapier, Make and n8n
Make and n8n work in every edition; Zapier is part of Enterprise.
All three receive the same JSON as a webhook. In SnagBane open the form → Automations → pick the service; the same three steps are shown there.
| Service | In the service | In SnagBane |
|---|---|---|
| Zapier | Create a Zap with the trigger Webhooks by Zapier → Catch Hook and copy the webhook URL Zapier shows. | Paste it as Catch Hook URL, create the automation, click Test, then Test trigger in Zapier: the answers appear as fields for the next steps. |
| Make | Create a scenario and add the Webhooks → Custom webhook module; click Add and copy the webhook address. | Paste it as Webhook URL, create the automation and click Test: Make learns the data structure, then add the next modules. |
| n8n | Add a Webhook node with the HTTP method POST; copy its Production URL (not the Test URL) and activate the workflow. | Paste it as Production webhook URL, create the automation, click Test and check the execution in n8n. |
Failed deliveries are retried; every attempt is listed under the automation's Log. To check that a call really came from SnagBane, verify the signature as described under Webhooks & signatures (n8n and Make can do it with a code step).
Microsoft Teams notifications
In Teams, open the chat or channel, then ⋯ → Workflows and pick “Send webhook alerts to a chat” or “Send webhook alerts to a channel”. Finish the wizard and copy the webhook URL. In SnagBane, add the Microsoft Teams integration (form → Integrations, or Integrations & API for all forms) and paste the URL. Each response then posts an Adaptive Card with the answers and an Open in SnagBane button.
Webhooks & signatures
SnagBane POSTs JSON for the events response.created, response.updated, approval.requested, approval.approved, approval.rejected, form.published and form.closed. A failed delivery is retried up to 3 times, and every attempt is logged. You can re-send any delivery from the log.
POST /your-endpoint
X-SnagBane-Event: response.created
X-SnagBane-Signature: sha256=5d7f…
{ "event": "response.created", "form": {"id": 2, "title": "…"},
"response": { "id": 91, "respondent": {"name": "Adele Vance", "email": "…"},
"fields": {"nps": 9, "rate_our_support": 5}, "answers": [ … ] } }
Every request also carries X-SnagBane-Timestamp and X-SnagBane-Signature-V2 (HMAC-SHA256 of timestamp.body), so receivers can reject replayed requests, and an X-SnagBane-Delivery id that stays the same across retries. Verify it in PHP:
$body = file_get_contents('php://input'); $ts = $_SERVER['HTTP_X_SNAGBANE_TIMESTAMP'] ?? '';
$valid = ctype_digit($ts) && abs(time() - (int) $ts) <= 300
&& hash_equals('sha256='.hash_hmac('sha256', $ts.'.'.$body, $secret), $_SERVER['HTTP_X_SNAGBANE_SIGNATURE_V2'] ?? '');
Examples for Node.js, Python and C#, and the full event list, are in the API reference.
REST API (Enterprise)
Read forms and responses from scripts and other systems with a personal API token (Automations & API → Personal API tokens). Base URL: https://your-domain/api/v1, header Authorization: Bearer sb_…, 120 requests per minute.
| Method & path | Description |
|---|---|
GET /me | The token's user |
GET /forms | List forms |
GET /forms/{id} | Form with its questions (key, label, type, options) |
GET /forms/{id}/responses?since=&top=&page= | Paginated responses, newest first |
GET /forms/{id}/responses/{responseId} | One response |
POST /hooks {form_id, url, event} | Subscribe a REST hook (returns its id and signing secret) |
DELETE /hooks/{id} | Unsubscribe |
GET /hooks/sample/{form} | Sample payload for mapping fields |
curl https://forms.example.com/api/v1/forms/2/responses?top=10 -H "Authorization: Bearer sb_xxx"
Full reference with examples (curl, PowerShell, Python), errors, pagination, answer formats and webhook verification: REST API reference →
Security features
- Two-factor authentication (TOTP) with one-time recovery codes; optionally required for admins. Microsoft / Google single sign-on.
- Account lockout after repeated failed logins, generic error messages, strong password policy, bcrypt hashing, CSRF protection and session-fixation protection.
- Strict Content-Security-Policy with per-request nonces, clickjacking protection (public forms stay embeddable), HSTS on https, nosniff and referrer policy.
- Uploads: extension allow-list, content sniffing, blocked scripts/executables/SVG/HTML and double extensions, random file names, private storage outside the web root.
- SSRF protection for all webhooks and integrations (private/metadata addresses blocked, DNS-rebinding safe), with an admin allow-list for trusted internal hosts.
- All secrets (Microsoft, Google, mail, integration credentials) encrypted with your APP_KEY; API tokens, approval links, edit and resume links stored only as hashes.
- Server-side re-validation of every answer, branching path and calculation; answer keys never leave the server; spreadsheet formula-injection protection in CSV, Excel and Google Sheets exports.
- Full audit log of sign-ins, admin actions, exports and approvals.
Send logs to a SIEM (Microsoft Sentinel, Elastic, Splunk…)
SnagBane writes everything a security team usually wants as JSON lines (one flat JSON object per line, UTF-8, appended, never rewritten) — the format the log agents of Microsoft, Elastic, Splunk and most other SIEMs read directly. Nothing is sent to anyone unless you set it up.
| What | Where | Contents |
|---|---|---|
| Audit log | storage/logs/audit/audit-YYYY-MM-DD.log (one file per UTC day) | Who did what: sign-ins and failed sign-ins (auth.login, auth.failed, auth.password_blocked, auth.recovery_code_used), users and roles, API tokens, settings, integrations and webhooks, domains, forms and portals, response deletions and exports, updates, licence, database password changes, audit exports. |
| Form events | storage/logs/forms/<form>.log — the Local log file integration on a form (Connect apps) | One line per submission, approval and the other events you pick. |
| Application errors | storage/logs/laravel-*.log; in Docker also the container output (docker compose logs) | Errors and warnings of the app itself. |
Audit record fields (names are stable; new fields may be added, existing ones are never renamed):
| Field | Type | Meaning |
|---|---|---|
time | string, ISO 8601 UTC | When it happened, e.g. 2026-10-01T08:15:02.113Z |
app / site | string | Always snagbane / the host name of the site address (to tell installations apart) |
id | number | Audit entry number (unique per installation, increasing) |
action | string | What happened, e.g. user.updated |
user_id, user_email, user_name | number / string, or null | Who did it (null for the system, e.g. scheduled jobs or failed sign-ins) |
subject_type, subject_id | string or null | What it was done to, e.g. Form / 42 |
ip | string or null | Client address as SnagBane saw it (behind a reverse proxy on a public address, set TRUSTED_PROXIES in .env so this is the real client; private networks are trusted by default) |
meta | string (JSON) | Details as a JSON text, e.g. {"to":"editor"} — kept as text so the line stays flat |
Example line:
{"time":"2026-10-01T08:15:02.113Z","app":"snagbane","site":"forms.contoso.com","id":5120,"action":"user.updated","user_id":1,"user_email":"admin@contoso.com","user_name":"Ana Admin","subject_type":"User","subject_id":"17","ip":"203.0.113.10","meta":"{\"role\":\"editor\"}"}
- Keeping files: day files older than 30 days are removed every night. Change it with
SNAGBANE_AUDIT_DAYS=90in.env;SNAGBANE_AUDIT_FILE=falseturns the files off (the audit log in the database stays). Keep at least a few days so your agent always has time to read a file. - Export: Administration → Audit & delivery logs → pick dates (UTC) → Export CSV or Export JSON lines — the same fields, for a one-off import or an auditor.
- Docker: the files are inside the
storagevolume. For an agent on the host, put the audit folder on the host with adocker-compose.override.ymlnext todocker-compose.yml, thendocker compose up -d. All three SnagBane containers write audit entries, so mount it in each:
The agent then readsservices: app: volumes: ["./audit-logs:/var/www/html/storage/logs/audit"] worker: volumes: ["./audit-logs:/var/www/html/storage/logs/audit"] scheduler: volumes: ["./audit-logs:/var/www/html/storage/logs/audit"]./audit-logs/audit-*.login the SnagBane folder on the host.
Microsoft Sentinel (Azure Monitor Agent)
Microsoft Sentinel works on a Log Analytics workspace. The Azure Monitor Agent reads the audit files into a custom table of that workspace with a Custom JSON Logs data collection rule (DCR). The server must be an Azure virtual machine or an Azure Arc-enabled server (for servers outside Azure); you need at least contributor rights on the workspace and permission to create DCRs. Steps from Microsoft's documentation Collect JSON file from virtual machine with Azure Monitor and Collect guest log data from virtual machines:
- Create the table first — Microsoft states the table must exist before the DCR and must be created with a PowerShell script like this one (Azure PowerShell,
Invoke-AzRestMethod). Replace the{…}parts:$tableParams = @' { "properties": { "schema": { "name": "SnagBaneAudit_CL", "columns": [ { "name": "TimeGenerated", "type": "dateTime" }, { "name": "Computer", "type": "string" }, { "name": "FilePath", "type": "string" }, { "name": "app", "type": "string" }, { "name": "site", "type": "string" }, { "name": "id", "type": "int" }, { "name": "action", "type": "string" }, { "name": "user_id", "type": "int" }, { "name": "user_email", "type": "string" }, { "name": "user_name", "type": "string" }, { "name": "subject_type", "type": "string" }, { "name": "subject_id", "type": "string" }, { "name": "ip", "type": "string" }, { "name": "meta", "type": "string" } ] } } } '@ Invoke-AzRestMethod -Path "/subscriptions/{subscription}/resourcegroups/{resourcegroup}/providers/microsoft.operationalinsights/workspaces/{WorkspaceName}/tables/SnagBaneAudit_CL?api-version=2021-12-01-preview" -Method PUT -payload $tableParams - In the Azure portal: Monitor → Data Collection Rules → Create. On Basics give a Rule Name, Subscription, Resource group and a Region that is the same as the workspace's. If the portal asks for a Data Collection Endpoint for this type of data (Help me choose next to Type of telemetry tells you), select or create one.
- Resources → Add resources → the SnagBane server. The portal installs the Azure Monitor Agent when needed.
- Collect and deliver → Add new dataflow → Data source type: Custom JSON Logs:
- File pattern: the audit folder with a wildcard in the file name, e.g.
/var/www/snagbane/storage/logs/audit/audit-*.log(Docker with the mount above:/opt/snagbane/audit-logs/audit-*.log; Windows:C:\snagbane\storage\logs\audit\audit-*.log). Wildcards are allowed only in the file name and the first folder above it. - Table name:
SnagBaneAudit_CL. - Transform: use SnagBane's own time as the record time:
source | extend TimeGenerated = todatetime(time) | project-away time - JSON Schema: the fields from the table above.
- Destination: Azure Monitor Logs → your Sentinel workspace.
- File pattern: the audit folder with a wildcard in the file name, e.g.
- Create the rule. Data can take up to 5 minutes to arrive. Check it in the workspace (Logs):
SnagBaneAudit_CL | take 10. From there, write Sentinel analytics rules on it, e.g. manyauth.failedfor oneip, oruser.updatedwheremetacontains"owner".
Microsoft's notes that apply: files must be UTF-8 JSON lines and appended (SnagBane does both); don't rename or copy files into the watched folder; keep the number of watched folders small.
Elastic (Filebeat or Elastic Agent)
Use a filestream input with the ndjson parser — from Elastic's filestream input reference. In filebeat.yml:
filebeat.inputs:
- type: filestream
id: snagbane-audit
paths:
- /var/www/snagbane/storage/logs/audit/audit-*.log
parsers:
- ndjson:
target: ""
add_error_key: true
- Each
filestreaminput needs its own uniqueid; don't change it later (Elastic warns this causes duplicates). target: ""puts SnagBane's fields at the top level of the event;add_error_keymarks lines that aren't valid JSON.- By default filestream starts reading a file only when it is larger than 1024 bytes, so the first entries of a new day can appear a little later on a quiet installation.
Splunk (universal forwarder)
Install the Splunk universal forwarder on the SnagBane server (or, with Docker, on the host that has the ./audit-logs folder from above) and connect it to your Splunk indexers or Splunk Cloud Platform as Splunk describes for your environment. Then:
- Create an index for SnagBane in Splunk (for example
snagbane), or use an existing one. - On the forwarder, open
$SPLUNK_HOME/etc/system/local/inputs.conf(create it if it doesn't exist) and add a monitor stanza for the audit folder. Use the real path of your installation:[monitor:///var/www/snagbane/storage/logs/audit] index = snagbane sourcetype = _json disabled = 0_jsonis Splunk's source type for JSON-formatted events; each line is one event. Add a second stanza forstorage/logs/formsif you also want form events. - Restart the forwarder (
$SPLUNK_HOME/bin/splunk restart) and check the input with$SPLUNK_HOME/bin/splunk list monitor. - In Splunk, search
index=snagbane action=auth.failedto see failed sign-ins.
Splunk also accepts the input from the command line on the forwarder (splunk add monitor <path>), which writes the same inputs.conf for you. Checked against Splunk's documentation “Monitor files and directories with inputs.conf” and “Configure the universal forwarder using configuration files”.
Other SIEMs
Any agent that monitors a file and parses JSON lines works: point it at storage/logs/audit/audit-*.log (and, if you like, storage/logs/forms/*.log) and treat each line as one JSON event whose time is the time field (ISO 8601, UTC).
To push form events straight to an HTTP collector instead of a file, use the HTTP request integration (your own URL, headers such as Authorization — stored encrypted — and a JSON body with merge tags) or a signed webhook.
FAQ & troubleshooting
"AADSTS50011: redirect URI mismatch"
Your site URL changed. Re-run setup, or add https://your-domain/auth/microsoft/callback to the app registration's redirect URIs.
The health check fails right after connecting
A new client secret can take up to a minute to become active. Wait a minute and click Run health check again.
Device-code sign-in is blocked
Your Conditional Access policy blocks the device-code flow. Use the PowerShell or Manual tab instead.
Uploads fail
Raise upload_max_filesize and post_max_size in PHP, and the Max upload size in Branding & general.
Emails aren't sent
Configure Administration → Email and use Send test email. For Microsoft 365 SMTP, SMTP AUTH must be enabled on the mailbox. You can use the Graph option instead.
Credits & licences
Laravel validation translations from Laravel-Lang (MIT). Laravel (MIT), Vue.js (MIT), Vue Router (MIT), Tailwind CSS (MIT), Lucide icons (ISC), SortableJS (MIT), node-qrcode (MIT), Microsoft Teams JavaScript client library (MIT), Inter font (SIL OFL 1.1). Microsoft, Microsoft 365, Entra ID, Teams and Power Automate are trademarks of Microsoft Corporation. SnagBane is not affiliated with Microsoft.