Custom Integration — PHP Lead Forms
Connect your website contact form to TCRM with a secure server-to-server HTTPS request. This guide is for PHP developers, agencies, and landing-page teams.
On this page
- Overview
- Architecture
- Before You Start
- Endpoint
- Request Fields
- Authentication
- external_id vs nonce
- Basic HTML Form
- PHP Integration
- Protecting Your Integration Secret
- UTM Attribution
- Retries and Idempotency
- API Responses
- Testing
- Using This With WordPress
- Troubleshooting
- Security Checklist
- What Not To Do
1. Overview
TCRM can receive leads from your public website through a dedicated inbound integration. After a visitor submits a contact form, your website server signs the payload and posts it to TCRM. The lead then appears in the customer's CRM pipeline.
This is a server-to-server integration. Browser JavaScript must never hold the integration secret or call the authenticated endpoint directly.
2. Architecture
Browser
↓
Customer website form
↓
Website SERVER-SIDE PHP
↓
HTTPS + HMAC authenticated request
↓
TCRM inbound endpoint
↓
Lead created in TCRM
3. Before You Start
Ask the TCRM administrator / customer success contact for:
- Your integration endpoint URL
- Your integration secret
- Any customer-specific required fields or naming conventions
- Confirmation that the integration is enabled for that customer
Store the secret in an environment variable or a PHP config file outside the public web root.
4. Endpoint
Use the endpoint provided for your customer. The public path pattern is:
POST https://YOUR-TCRM-DOMAIN/api/v1/integrations/YOUR-INTEGRATION/leads
Replace YOUR-TCRM-DOMAIN and YOUR-INTEGRATION with the values issued for your project.
Always use HTTPS.
| Item | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Body | JSON object (UTF-8) |
| Auth | HMAC-SHA256 request signature |
5. Request Fields
{
"external_id": "11111111-1111-4111-8111-111111111111",
"name": "Jane Doe",
"phone": "+905321234567",
"email": "jane@example.com",
"message": "I would like more information",
"source": "Website",
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "summer_campaign",
"utm_adgroup": "villa_ads",
"utm_keyword": "villa istanbul",
"referrer_url": "https://example.com/?utm_source=google",
"submitted_at": "2026-08-09T10:00:00+03:00"
}
Required
external_id— UUID v4 that uniquely identifies this form submissionname— visitor full namephone— visitor phone number
Optional
email,message,sourceutm_source,utm_medium,utm_campaign,utm_adgroup,utm_keywordreferrer_url,submitted_at
Do not send privileged CRM fields such as owner, team, stage, or account identifiers. Assignment is controlled by TCRM for your customer.
6. Authentication
Every request must include these headers:
| Header | Description |
|---|---|
X-TCRM-Timestamp | Unix timestamp in seconds (string) |
X-TCRM-Nonce | Unique value for this HTTP attempt (UUID recommended) |
X-TCRM-Signature | Lowercase hex HMAC-SHA256 signature |
Build the canonical string exactly as:
timestamp + "." + nonce + "." + raw_request_body
Then calculate:
signature = HMAC-SHA256(secret, canonical_string) → lowercase hex
- Use the exact JSON bytes you will send in the POST body.
- Do not re-encode JSON after signing.
- Timestamps outside the allowed window are rejected.
- A reused nonce is rejected.
7. external_id vs nonce
external_id
Identifies one form submission. Keep it the same when retrying the same lead after a timeout.
X-TCRM-Nonce
Identifies one HTTP request attempt. Generate a new nonce for every attempt, including retries.
First attempt:
external_id = abc-123
nonce = nonce-1
Retry after timeout:
external_id = abc-123 (SAME)
nonce = nonce-2 (NEW)
8. Basic HTML Form
No API secret belongs in HTML, JavaScript, or browser storage.
9. PHP Integration
Example private config outside the public web root (config/tcrm.php):
getenv('TCRM_LEAD_ENDPOINT'),
'secret' => getenv('TCRM_INTEGRATION_SECRET'),
];
Example submit-lead.php (conceptual):
true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
'X-TCRM-Timestamp: ' . $timestamp,
'X-TCRM-Nonce: ' . $nonce,
'X-TCRM-Signature: ' . $signature,
],
CURLOPT_POSTFIELDS => $json, // EXACT signed body — do not re-encode
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$body = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
return ['code' => $code, 'body' => $body, 'error' => $err];
}
$name = trim((string)($_POST['name'] ?? ''));
$phone = trim((string)($_POST['phone'] ?? ''));
$email = trim((string)($_POST['email'] ?? ''));
$message = trim((string)($_POST['message'] ?? ''));
if ($name === '' || $phone === '') {
http_response_code(400);
exit('Name and phone are required.');
}
// Create external_id ONCE for this form submission.
$payload = [
'external_id' => uuid_v4(),
'name' => $name,
'phone' => $phone,
'email' => $email,
'message' => $message,
'source' => 'Website',
'utm_source' => trim((string)($_POST['utm_source'] ?? '')),
'utm_medium' => trim((string)($_POST['utm_medium'] ?? '')),
'utm_campaign' => trim((string)($_POST['utm_campaign'] ?? '')),
'utm_adgroup' => trim((string)($_POST['utm_adgroup'] ?? '')),
'utm_keyword' => trim((string)($_POST['utm_keyword'] ?? '')),
'referrer_url' => substr((string)($_SERVER['HTTP_REFERER'] ?? ''), 0, 2000),
'submitted_at' => date('c'),
];
// Encode JSON EXACTLY ONCE. Sign and send these same bytes on every attempt.
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$maxAttempts = 3;
$result = ['code' => 0, 'error' => 'not_attempted'];
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
$result = post_tcrm_lead($endpoint, $secret, $json);
$code = $result['code'];
// Never log secrets, signatures, or full PII — status only.
error_log('TCRM lead post attempt=' . $attempt . ' status=' . $code);
if ($code === 201 || $code === 200) {
header('Location: /thank-you.php');
exit;
}
// Do not retry client/auth/validation errors.
if (in_array($code, [400, 401, 403, 422], true)) {
break;
}
// Retry temporary failures: transport error, 429, selected 5xx.
$temporary = ($result['error'] !== '') || $code === 429 || ($code >= 500 && $code <= 599);
if (!$temporary || $attempt === $maxAttempts) {
break;
}
usleep(250000 * $attempt); // brief backoff
}
http_response_code(502);
exit('We could not submit your request right now. Please try again.');
10. Protecting Your Integration Secret
Good
- Environment variables
- Server secret manager
- PHP config outside the public web root
- Restrictive file permissions
Bad
- JavaScript / browser fetch
- HTML source
- Git repository
- Frontend env files
- Google Tag Manager
- Query parameters, cookies, localStorage
11. UTM Attribution
Pass marketing parameters from the landing URL into hidden form fields, then include them in the JSON payload. This helps sales and marketing teams understand which campaigns generate CRM leads.
Supported optional fields: utm_source, utm_medium, utm_campaign,
utm_adgroup, utm_keyword, referrer_url.
12. Retries and Idempotency
Retry only temporary conditions such as connection failures, timeouts, HTTP 429, or selected 5xx responses.
Do not blindly retry HTTP 400, 401, 403, or 422.
For every retry: generate a new nonce, keep the same external_id, and re-sign the request.
13. API Responses
New lead — HTTP 201
{"success": true, "duplicate": false, "lead_id": 123}
Idempotent retry — HTTP 200
{"success": true, "duplicate": true, "lead_id": 123}
Authentication failure — HTTP 401
{"success": false, "error": "unauthorized"}
Validation — HTTP 422
{"success": false, "error": "validation_failed"}
Rate limit — HTTP 429
{"success": false, "error": "rate_limited"}
Integration disabled — HTTP 503
{"success": false, "error": "integration_disabled"}
14. Testing
- Integration provisioned in TCRM
- Secret stored server-side
- Endpoint configured
- Send a recognizable test lead
- Receive HTTP 201
- Verify the lead appears in TCRM
- Resend the same
external_idwith a new nonce - Receive HTTP 200 with
duplicate: true - Confirm only one lead exists
- Test an invalid signature and confirm HTTP 401
- Verify the production form still shows the thank-you page
15. Using This With WordPress
You can connect custom WordPress forms, Elementor form hooks, Contact Form 7 hooks, or WPForms hooks. Regardless of the form plugin, the authenticated TCRM request must still run in server-side PHP.
Do not place the integration secret in Elementor custom JS, theme frontend scripts, or browser console snippets.
16. Troubleshooting
| Symptom | Likely causes |
|---|---|
401 unauthorized |
Wrong secret, incorrect canonical string, body changed after signing, expired timestamp, reused nonce |
422 validation_failed |
Missing name/phone, invalid field format, unexpected fields |
429 rate_limited |
Too many requests from your server IP |
503 integration_disabled |
Integration not enabled for the customer yet |
| cURL connection error | DNS, outbound firewall, TLS problem, timeout |
| Unexpected duplicate lead | A new external_id was generated during a retry |
| Signature fails with Turkish characters | Encode JSON once as UTF-8, sign those exact bytes, send the same string |
17. Security Checklist
- HTTPS only
- Server-to-server only
- Secrets outside the browser
- HMAC authentication
- Unique nonce per attempt
- Fresh timestamp
- UUID
external_idper submission - Exact-body signing
- Short connect/HTTP timeouts
- Safe retry strategy
- No PII or secrets in application logs
- Do not disable TLS verification
- Do not log signatures
- Do not expose raw API errors to website visitors
18. What Not To Do
Do not do this:
// BAD — never call TCRM with the secret from the browser
fetch("https://YOUR-TCRM-DOMAIN/api/v1/integrations/YOUR-INTEGRATION/leads", {
method: "POST",
headers: {
"X-TCRM-Signature": "SECRET_IN_THE_BROWSER"
}
});