Skip to Content
Custom Integration

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.


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.

ItemValue
MethodPOST
Content-Typeapplication/json
BodyJSON object (UTF-8)
AuthHMAC-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 submission
  • name — visitor full name
  • phone — visitor phone number

Optional

  • email, message, source
  • utm_source, utm_medium, utm_campaign, utm_adgroup, utm_keyword
  • referrer_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:

HeaderDescription
X-TCRM-TimestampUnix timestamp in seconds (string)
X-TCRM-NonceUnique value for this HTTP attempt (UUID recommended)
X-TCRM-SignatureLowercase 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

  1. Integration provisioned in TCRM
  2. Secret stored server-side
  3. Endpoint configured
  4. Send a recognizable test lead
  5. Receive HTTP 201
  6. Verify the lead appears in TCRM
  7. Resend the same external_id with a new nonce
  8. Receive HTTP 200 with duplicate: true
  9. Confirm only one lead exists
  10. Test an invalid signature and confirm HTTP 401
  11. 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

SymptomLikely 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_id per 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"
  }
});