← Blog
Perfex CRMWordPressIntegrations

Contact Form 7 to Perfex CRM: Send Submissions as Leads

Wire Contact Form 7 to Perfex CRM as real leads: a create-only token, the wpcf7_mail_sent hook, idempotent retries, and when to drop the PHP for Zapier.

The enquiry form on the website has been running for two years. Submissions arrive as email, get read on a phone, and roughly one in five gets a reply the same day. Nobody can say how many came in last month, which landing page they came from, or which ones turned into invoices, because not one of them ever became a record in anything.

Connecting Contact Form 7 to Perfex CRM fixes that with about forty lines of PHP, and the forty lines are the easy part. What decides whether the integration is still working in six months is the token you issue, what happens when the API call fails at 11pm, and whether a double-clicked submit button creates one lead or two.

We build and sell the Perfex REST API module, the Zapier module and WordFex, all of which appear below, so weigh the recommendations with that in mind. The mechanics apply to any WordPress form to CRM integration; only the endpoint names change.

Why a sign-up sync plugin will not catch your enquiry form

The obvious first move is to reach for a sync plugin, and for the wrong problem it is the right tool. WordFex captures every new sign-up on a WordPress or WooCommerce site and creates the matching record in Perfex, mapping first name, last name, email address and website, with a single setting deciding whether new sign-ups land in Leads or in Customers.

That is registration traffic. A contact form submission creates no WordPress user, so there is nothing for a sign-up sync to observe. If your site has both a membership area and an enquiry form, you want both paths - genuinely separate jobs, not one job configured twice.

The second path is the one this article builds: catch the submission in PHP and post it straight to the leads endpoint.

Issue the token before you write any code

The usual version of this integration reuses the master API key, stored in a theme file, with full access to the whole CRM. A WordPress site is the most attacked surface you own. Assume the key leaks and decide now what it can do when it does.

The REST API module for Perfex CRM grants or revokes Get, Create, Update and Delete access per resource, so a token for this job should hold exactly one grant: Create on Leads. Not Get - a create-only token cannot be used to enumerate your pipeline. Not Update, not Delete. The permission editor has a Clear all button and a live count of what is selected, so verifying that a token holds one permission takes seconds.

Three settings on the same screen are worth setting now:

  • An expiry date. A token that dies in twelve months forces a review you would otherwise never do.
  • Request and quota limits. A form endpoint should never need thousands of calls a day. Cap it, and a compromised key or a runaway loop stops being unbounded.
  • IP whitelisting, which supports CIDR notation. If the site posts from one or two fixed web-server addresses, allowlist them.

For transport, send the key as a Bearer token in the Authorization header. The module also accepts it as an authtoken or api_key query parameter, which is convenient for a quick test but writes your credential into every access log along the path. Use the header in production.

One thing to know before debugging: brute-force protection throttles repeated failed authentications per IP, so a bad token deployed to a busy site can get your own web server throttled - which reads as an outage rather than a typo.

Wiring Contact Form 7 to Perfex CRM with wpcf7_mail_sent

Contact Form 7 exposes the submission through WPCF7_Submission::get_instance(). The hook most tutorials reach for is wpcf7_mail_sent, which runs after the notification email has gone out successfully.

add_action( 'wpcf7_mail_sent', 'ts_cf7_lead_to_perfex' );

function ts_cf7_lead_to_perfex( $contact_form ) {
    if ( 742 !== (int) $contact_form->id() ) {
        return; // only the enquiry form, not every form on the site
    }

    $submission = WPCF7_Submission::get_instance();
    if ( ! $submission || 'spam' === $submission->get_status() ) {
        return;
    }

    $d = $submission->get_posted_data();

    $payload = array(
        'name'        => sanitize_text_field( $d['your-name'] ?? '' ),
        'email'       => sanitize_email( $d['your-email'] ?? '' ),
        'phonenumber' => sanitize_text_field( $d['your-phone'] ?? '' ),
        'company'     => sanitize_text_field( $d['company'] ?? '' ),
        'description' => sanitize_textarea_field( $d['your-message'] ?? '' ),
    );

    $res = wp_remote_post( PERFEX_API . '/leads', array(
        'timeout' => 8,
        'headers' => array(
            'Authorization'   => 'Bearer ' . PERFEX_LEAD_TOKEN,
            'Content-Type'    => 'application/json',
            'Idempotency-Key' => md5( $d['your-email'] . $submission->get_meta( 'timestamp' ) ),
        ),
        'body'    => wp_json_encode( $payload ),
    ) );

    ts_cf7_handle_perfex_response( $res, $payload );
}

Do not guess the field names. The module publishes an OpenAPI 3.0 specification at GET /api/openapi - 74 paths and 144 operations, every request body documenting its fields, types and required flags across 498 typed fields - plus an interactive Swagger playground inside Perfex. Post one test lead from there, copy the body that worked, and only then write the PHP.

The hook that stops firing when your mail does

wpcf7_mail_sent runs only when the email was sent. If SMTP breaks - an expired app password, a host blocking port 587 - the hook never fires, and your CRM stops receiving leads at the exact moment your inbox does. One failure hides behind the other.

wpcf7_before_send_mail runs earlier, before the mail step, so it survives a broken mailer. The trade-off is that you take on the status check yourself, which is why the snippet above bails on spam. If the CRM record matters more than the notification email - and for anything sales-facing it does - move to the earlier hook and keep the guard.

The four failures that only appear in production

Everything above works on the first test. These surface later.

SymptomCauseFix
Two identical leads, seconds apartImpatient double submit, or a retry after a timeoutIdempotency-Key on the POST
Enquiry in the inbox, nothing in the CRMThe API call failed and nobody was toldLog the payload, schedule a retry
Leads stop arriving after a traffic spikeRequest or quota limit reachedRead the X-RateLimit headers
Long messages rejectedMaximum request body sizeRaise the limit, or truncate before posting

Duplicates. The module supports Idempotency-Key on POST: an identical retry safely replays the stored response instead of executing again, within a 24-hour window. Derive the key from something stable - the email plus the submission timestamp works - and a double click, a browser refresh and your own retry logic all collapse to one lead.

Silent failure. wp_remote_post() returns a WP_Error on a connection failure, and an HTTP 4xx is not an error at all as far as WordPress is concerned. Check both. On failure, write the payload somewhere durable and schedule a retry with wp_schedule_single_event(), or Action Scheduler if WooCommerce is already loaded. An enquiry that reached your inbox but not your pipeline is the worst outcome here, because nothing looks broken. If a backlog does accumulate, the module’s batch operations endpoint takes up to 50 operations per request with per-operation results and continue-on-error - a far better way to drain a queue than fifty separate calls.

Rate limits. Every response carries X-RateLimit headers, so a client never has to guess where it stands. Read them and back off rather than hammering an endpoint that is already refusing you. To confirm calls are arriving at all, request logging captures every call and the reporting screen breaks statistics down per API key.

On the successful path, the create response returns record_id - the ID of the record you just created, straight back from the POST. Store it against the submission. It is what lets you answer “was this enquiry ever followed up” without a manual search, and it is free at the moment of creation and expensive to reconstruct afterwards.

What a four-field payload leaves you to solve

A working POST is not the same as a useful CRM record. Three gaps are worth closing on day one:

  1. Attribution. Add hidden fields carrying utm_source, utm_medium, utm_campaign and the landing page URL, then map them into named Perfex custom fields. The module exposes custom fields directly - add, update, delete, request fields and their values, and search field values - so campaign data becomes something you can group and filter by, instead of a line buried in the message body where no report can reach it.
  2. Context. The Notes endpoint attaches polymorphic notes to 12 entity types, leads included, and file attachments can be pushed to leads. A form with an upload field can therefore land the brief on the lead record rather than only in the notification email.
  3. Spam. Contact Form 7 with no spam protection will fill your pipeline with junk, and a junk lead costs more than a junk email because a person has to open it and decide. Turn on the honeypot or a challenge before you turn on the sync.

When to stop writing PHP

The hand-rolled route is right when you control the site, the mapping is fixed and you want nobody else in the path. It stops being right the moment the requirement grows a second destination.

The rule: if the submission has to reach anything other than Perfex, stop writing PHP. One form to one CRM is forty lines. One form to a CRM, a Slack channel, a mailing list and a spreadsheet is a small application, and you will own it forever.

That is where the Zapier module for Perfex CRM earns its place. It registers Perfex as a native app on Zapier with a Create Lead action, so a Zap can create the lead and fan the same submission across the 7,000+ apps Zapier reaches, with retry and error handling already built. It also ships Find Customer, which matters when the enquiry comes from somebody you already bill and you would rather match than duplicate. The REST API module carries native Make.com and n8n connectors too.

The build order

  1. Issue a token with Create on Leads only, an expiry date and a request quota. Nothing else.
  2. Post one test lead from the Swagger playground. Keep the exact body that worked.
  3. Write the hook against wpcf7_before_send_mail, guarding on form ID and submission status.
  4. Add the Idempotency-Key header, derived from email plus submission timestamp.
  5. Handle both WP_Error and non-2xx responses: log the payload, schedule one retry.
  6. Store the returned record_id against the submission.
  7. Add hidden UTM fields and map them to named custom fields.
  8. Submit the live form twice in quick succession and confirm you have one lead, not two.

Step eight is the one everybody skips, and it is the only one that proves the integration will hold up on the day a real prospect gets impatient.