A lightweight multi-site inquiry / lead management system built with pure PHP + MySQL. It centralizes form submissions from multiple websites into one admin panel, where your team can review, assign, filter, tag, follow up, export, and audit every inquiry in one place.
Suitable for companies operating multiple marketing sites, contact forms, quote forms, sample request forms, distributor forms, and other lead-capture pages.
- Project Overview
- Main Use Cases
- Core Features
- Tech Stack
- Project Structure
- How the System Works
- Quick Start
- Configuration
- Database Setup
- Deployment Notes
- Admin Routes
- Public API
- Standard Inquiry Fields
- Site Onboarding Checklist
- Multi-site Form Submission Examples
- Field Mapping Examples
- Email Notification Notes
- Security Notes
- Testing and Release Checks
- Upgrade Notes
- Troubleshooting
- Suggested GitHub Repository Details
- License / Internal Use
Inquiry Management System is designed to solve a common business problem:
- You have multiple websites
- Each site has one or more forms
- Form fields are not always identical
- Leads are scattered across emails, site backends, or spreadsheets
- Sales and operations teams need one place to review and follow up
This project provides a central system that receives inquiries from multiple websites through a unified API and stores them in a shared database. The admin backend then provides:
- inquiry list and detail pages
- assignment and follow-up workflow
- spam / blacklist control
- logs and API request tracing
- CSV export and reusable export templates
- site-specific API credentials and notification rules
This system is a good fit if you need to collect inquiries from websites such as:
- Main corporate website contact forms
- Product inquiry / RFQ forms
- Free sample request forms
- Distributor application forms
- Dealer application forms
- Landing pages for campaigns
- Regional sites for different countries or brands
Example multi-site setup:
a.com→ Main company siteb.com→ Sample request sitec.com→ Distributor recruitment site
All submissions go into one admin system.
- Unified inquiry submission API
- One site = one
site_key - Site-specific
api_token - Optional HMAC request signature
- Site-level field mapping
- Site-level notification override
- Inquiry list
- Inquiry detail view
- Status update (
unread,read,spam,trash) - Assign owner
- Admin note
- Follow-up history and reminders
- Bulk actions
- Blacklisted IPs
- Blacklisted emails and domains
- Honeypot support
- Link count threshold
- Duplicate submission window
- IP / email rate limits
- Keyword rules
- Country blocks
- Content length rules
- CSV export
- Select export columns
- Save reusable export templates
- Dashboard summary
- Reports and analytics
- Follow-up reminders
- System logs
- API request logs
- Release check scripts
- Manual test checklist
- API example scripts
- Create admin user
- Edit admin profile
- Reset password
- Enable / disable admin user
- Role and status management
- Backend: PHP 8+
- Database: MySQL / MariaDB
- Frontend: server-rendered PHP views + Bootstrap-based admin UI
- Architecture: lightweight MVC-style structure, no heavy framework
.
├── app/
│ ├── Controllers/
│ ├── Core/
│ ├── Helpers/
│ ├── Models/
│ └── Services/
├── bootstrap/
├── config/
├── database/
│ ├── schema.sql
│ ├── seed.sql
│ └── upgrade-v*.sql
├── examples/
│ ├── api-tests/
│ ├── javascript-fetch-example.js
│ ├── php-forwarder.php
│ └── php-signed-forwarder.php
├── public/
│ ├── index.php
│ └── assets/
├── resources/
│ └── views/
├── scripts/
│ ├── check-release.php
│ ├── check-release.sh
│ └── check-release.bat
└── storage/
A user fills in a form on one of your external websites.
Depending on your setup, the site may:
- send the request directly with JavaScript (token-only sites)
- or send it through the site backend (recommended)
- or send it with HMAC signature (recommended for stronger security)
The system checks:
- site key
- API token
- optional signature
- required fields
- anti-spam rules
The system stores:
- standard fields in dedicated columns
- extra fields in
extra_data - original request snapshot in
raw_payload
Your team can then:
- review the inquiry
- assign an owner
- update the status
- add follow-up records
- export filtered lists
- inspect API logs if something fails
- PHP 8.0 or above
- MySQL 5.7+ or MariaDB with JSON support recommended
- Apache or Nginx
- PDO MySQL extension enabled
curlextension recommended for example forwarders
- Upload project files to your server
- Point your web root to the
public/directory - Create a database
- Import:
database/schema.sqldatabase/seed.sql
- Review
config/database.php - Review
config/app.php - Visit:
/public/login- or your configured base URL
Important keys:
name→ system namebase_url→ currently set to/public/timezonedebugsession_nameapidefaults:- signature tolerance
- honeypot field
- default rate limits
- duplicate window
This file stores the current database connection.
Keep the existing connection values already used in your deployed/test environment unless you intentionally need to change them.
Import in this order:
source database/schema.sql;
source database/seed.sql;Use the corresponding upgrade files in:
database/upgrade-v0.3.0.sql
database/upgrade-v0.4.0.sql
...
database/upgrade-v0.8.5.sql
For small UI-only or maintenance releases, the upgrade file may be a no-op placeholder indicating that no schema change is required.
Point your domain or virtual host to:
/public
http://localhost/your-project/public/
The current config/app.php uses:
'base_url' => '/public/'If you later deploy to a subdomain or different document root, update it accordingly.
Main admin pages in the current project:
/dashboard/reports/stats/inquiries/inquiry?id=1/followup-reminders/sites/sites/edit?id=1/admins/admins/edit?id=1/logs/api-logs/api-log?id=1/tools/blacklist-ips/tools/blacklist-emails/tools/spam-rules/tools/email-notifications/profile
GET /api/v1/health
Example:
curl -X GET "https://your-central-domain.com/api/v1/health"POST /api/v1/inquiries/submit
Supports:
application/json- standard form-style POST submissions handled by your site backend
The system is designed around a mix of standard fields and flexible extra fields.
These are the most common fields used across websites:
site_keyapi_tokenform_keynameemailtitlecontentcountryphoneaddressfrom_companysource_urlreferer_urlclient_ipbrowserlanguagesubmitted_atextra_data(JSON object)
At minimum, most sites should send:
site_keyapi_tokennameemailcontent
Site-specific fields should go into:
extra_data
Examples:
{
"product_interest": "WPC Decking",
"sample_pack": "Yes",
"project_stage": "Planning",
"quantity": "200 sqm"
}When connecting a new website, follow this checklist:
- Create the site in Sites & API
- Record these values:
site_keyapi_token- optional
signature_secret
- Decide whether the site requires signed requests
- Set
form_keynames for each form - Configure field mapping if the source form uses different field names
- Configure site-specific notification override if needed
- Submit a test inquiry
- Check:
- inquiry list
- inquiry detail
- API request logs
- system logs
Below are practical submission patterns for different kinds of websites.
This is the safest and most common setup.
Flow:
- User submits form on
a.com a.comvalidates form locallya.combackend forwards payload to central IMS API- IMS stores the inquiry
Use file:
examples/php-forwarder.php
Example:
<?php
$apiEndpoint = 'https://your-central-domain.com/api/v1/inquiries/submit';
$payload = [
'site_key' => 'a_main',
'api_token' => 'token_a_main_2026',
'form_key' => 'contact_form',
'name' => $_POST['name'] ?? '',
'email' => $_POST['email'] ?? '',
'title' => $_POST['subject'] ?? '',
'content' => $_POST['message'] ?? '',
'country' => $_POST['country'] ?? '',
'phone' => $_POST['phone'] ?? '',
'from_company' => $_POST['company'] ?? '',
'source_url' => 'https://a.com/contact-us/',
'client_ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'browser' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
'submitted_at' => date('c'),
'extra_data' => [
'product_interest' => $_POST['product_interest'] ?? '',
'quantity' => $_POST['quantity'] ?? '',
],
];
$ch = curl_init($apiEndpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);Recommended for:
- PHP sites
- custom sites
- Laravel / CodeIgniter / old PHP sites
- websites where token secrecy matters
For more sensitive sites, enable require_signature and send:
X-TimestampX-Signature
Use file:
examples/php-signed-forwarder.php
Signature rule:
signature = HMAC_SHA256(timestamp + "\n" + raw_body, signature_secret)
Example:
<?php
$endpoint = 'https://your-central-domain.com/api/v1/inquiries/submit';
$siteKey = 'b_sample';
$apiToken = 'token_b_sample_2026';
$signatureSecret = 'sig_b_sample_2026_secret_1234567890';
$payload = [
'site_key' => $siteKey,
'api_token' => $apiToken,
'form_key' => 'sample_form',
'name' => 'John Smith',
'email' => 'john@example.com',
'title' => 'Request for free samples',
'content' => 'Please send us your decking sample options.',
'country' => 'United States',
'source_url'=> 'https://b.com/free-samples/',
'extra_data'=> [
'product_interest' => 'Decking',
'project_stage' => 'Planning',
],
];
$rawBody = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$timestamp = (string) time();
$signature = hash_hmac('sha256', $timestamp . "\n" . $rawBody, $signatureSecret);
$ch = curl_init($endpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
],
CURLOPT_POSTFIELDS => $rawBody,
]);Recommended for:
- business-critical lead sites
- signed server-to-server integrations
- cases where direct frontend submission is not acceptable
This works for simple sites, but is less secure because the token is exposed in frontend JavaScript.
Use file:
examples/javascript-fetch-example.js
Example:
fetch('https://your-central-domain.com/api/v1/inquiries/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
site_key: 'a_main',
api_token: 'token_a_main_2026',
form_key: 'contact_form',
name: 'John Smith',
email: 'john@example.com',
content: 'I want more information about your products.',
extra_data: {
product_interest: 'Decking'
}
})
})
.then(r => r.json())
.then(console.log)
.catch(console.error);Use only when:
- security requirements are low
- the site is static
- you accept that the token is visible in source code
This is often the cleanest pattern for marketing sites.
<form method="post" action="/submit-contact.php">
<input type="text" name="name" placeholder="Name" required>
<input type="email" name="email" placeholder="Email" required>
<input type="text" name="phone" placeholder="Phone">
<input type="text" name="company" placeholder="Company">
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form><?php
$apiEndpoint = 'https://your-central-domain.com/api/v1/inquiries/submit';
$payload = [
'site_key' => 'a_main',
'api_token' => 'token_a_main_2026',
'form_key' => 'contact_form',
'name' => trim($_POST['name'] ?? ''),
'email' => trim($_POST['email'] ?? ''),
'phone' => trim($_POST['phone'] ?? ''),
'from_company' => trim($_POST['company'] ?? ''),
'content' => trim($_POST['message'] ?? ''),
'source_url' => 'https://a.com/contact/',
'submitted_at' => date('c'),
];
$ch = curl_init($apiEndpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$response = curl_exec($ch);
curl_close($ch);
header('Location: /thank-you.html');
exit;Recommended for:
- classic marketing sites
- brochure sites
- sites where you want full control over validation and thank-you flow
For a sample request form, send standard fields plus extra_data:
{
"site_key": "b_sample",
"api_token": "token_b_sample_2026",
"form_key": "sample_form",
"name": "Jane Doe",
"email": "jane@example.com",
"title": "Free sample request",
"content": "Please send color samples for our upcoming project.",
"country": "Australia",
"phone": "+61 400 000 000",
"from_company": "Acme Projects",
"source_url": "https://b.com/free-samples/",
"extra_data": {
"product_interest": "Decking",
"sample_pack": "Yes",
"preferred_colors": ["Teak", "Smoke Grey"],
"project_stage": "Planning",
"quantity": "150 sqm"
}
}A dealer form often has more fields than the core schema. Send them in extra_data.
{
"site_key": "c_distributor",
"api_token": "token_c_distributor_2026",
"form_key": "quote_form",
"name": "Michael Brown",
"email": "dealer@example.com",
"title": "Distributor application",
"content": "We would like to discuss dealership opportunities.",
"country": "United Kingdom",
"phone": "+44 20 0000 0000",
"from_company": "Brown Building Supply",
"source_url": "https://c.com/distributor/",
"extra_data": {
"annual_volume": "5000 sqm",
"sales_region": "London & South East",
"warehouse_available": "Yes",
"team_size": "12",
"current_products": "Decking, cladding, fencing"
}
}Useful for quick API tests.
curl -X POST "https://your-central-domain.com/api/v1/inquiries/submit" \
-H "Content-Type: application/json" \
-d '{
"site_key": "a_main",
"api_token": "token_a_main_2026",
"form_key": "contact_form",
"name": "John Smith",
"email": "john@example.com",
"content": "Please contact us about your products.",
"extra_data": {"product_interest": "Decking"}
}'Also see:
examples/api-tests/health-check.sh
examples/api-tests/submit-valid.sh
examples/api-tests/submit-invalid-token.sh
Not every website uses the same field names. That is exactly why field mapping exists.
A site may submit:
fullnameuser_emailmessagecompany_name
But the system standard fields are:
nameemailcontentfrom_company
Configure this in the site record:
{
"name": ["fullname", "your_name"],
"email": ["user_email", "contact_email"],
"title": ["subject"],
"content": ["message", "comments"],
"from_company": ["company", "company_name"],
"phone": ["mobile", "tel"]
}If the API receives:
{
"site_key": "b_sample",
"api_token": "token_b_sample_2026",
"fullname": "Leo Liu",
"user_email": "leo@example.com",
"message": "Need product samples.",
"company_name": "Example Co."
}The system can still map it into:
name = Leo Liuemail = leo@example.comcontent = Need product samples.from_company = Example Co.
The system supports:
- global notification settings
- site-level notification override
log_onlymode for testingmailmode using PHPmail()
Recommendation:
- use
log_onlyfirst in testing - only switch to
mailafter your server mail setup is confirmed
The safest production model is:
- each website submits to its own backend
- the backend forwards to IMS
- signed requests enabled for important sites
If possible, do not expose:
api_tokensignature_secret
in browser JavaScript.
Use:
require_signature = 1X-TimestampX-Signature
At minimum, enable:
- honeypot
- IP rate limit
- email rate limit
- duplicate window
- email/domain blacklist
Useful files already included in the repository:
RELEASE-CHECKLIST.mdMANUAL-TEST-CHECKLIST.mdAPI-TEST-EXAMPLES.mdscripts/check-release.phpscripts/check-release.shscripts/check-release.bat
php scripts/check-release.phpThis helps detect:
- missing classes
- route / controller issues
- view file problems
- release consistency issues
Current release history includes:
v0.3.x→ site management / signatures / logsv0.4.x→ field mapping / spam rule center / admin notev0.5.x→ email notifications / email blacklist / export field controlv0.6.x→ follow-ups / assignees / bulk actions / analyticsv0.7.x→ admin roles / API request logs / advanced spam rulesv0.8.x→ UI rebuild and stabilizationv0.8.5→ admin user edit and safe disable flow
For production upgrades, always:
- back up the database
- back up current project files
- apply code update
- run the matching upgrade SQL
- test key pages
- test one valid API submission
Check:
- API response body
/api-logs/logs- site
api_token - site
status - spam rules
Check:
- honeypot field
- link threshold
- blacklisted email / domain
- keyword rules
- duplicate / rate limit windows
Check:
require_signatureenabled?- timestamp tolerance
- exact raw JSON body used in signature
- newline format:
timestamp + "\n" + raw_body - secret mismatch
Check:
- global notifications enabled?
- transport set to
mail? - server supports PHP
mail()? - site notification override set to
disable? - logs for
notification_failed
Check:
status = active- password reset if needed
- self-disable protection / admin count protection rules
These values are ready to copy into the GitHub About panel.
Multi-site inquiry management system built with PHP + MySQL. Collect, review, assign, follow up, export, and audit form submissions from multiple websites in one admin panel.
php, mysql, inquiry-management, lead-management, contact-form, form-api, multi-site, admin-dashboard, crm-lite, bootstrap-5
Use your deployed central system URL, for example:
https://your-domain.com/public/
If you want a cleaner public-facing URL later, move the app to a dedicated subdomain such as:
https://ims.yourdomain.com/
and update config/app.php accordingly.
This repository is currently best treated as:
- company internal project
- custom business system
- private operational tool
If you later plan to open source it, add a proper license file and review:
- credentials
- seed data
- test tokens
- private domains
- example email addresses