The first question I asked when I started building automated follow-up at NewFed was not "which CRM should I use." It was "what kills this legally before it ever touches a borrower?" That question leads directly to TCPA compliant mortgage automation as a design constraint, not a checkbox. The Telephone Consumer Protection Act sets a hard ceiling on what you can send, when you can send it, and what happens if your system misses an opt-out. The ceiling is $500 per violation for negligent violations and $1,500 per violation for willful ones. In a high-volume mortgage shop running automated SMS sequences, that math gets ugly fast.

This post walks through the actual technical build: how consent gets collected and stored, how the send-window check works across time zones, how opt-outs fire a webhook that immediately stops every downstream touchpoint, and where I have seen the system break in production. This is not a vendor feature matrix. It is the architecture I designed and the tradeoffs I made.

What TCPA Actually Requires for Mortgage Outreach

Most mortgage compliance content stops at "get consent before texting." That is necessary but not sufficient. Here is what the rule actually demands, broken into the pieces that affect system design.

Prior Express Written Consent

For autodialed or prerecorded calls and texts to a mobile number, you need prior express written consent. The FCC's 2023 one-to-one consent rule, which took effect in January 2025, tightened this further: the consent must name the specific seller, not just a lead aggregator. If you are buying leads from a third-party form, you need to verify the form language named your company or your brand. If it did not, that consent is not yours to use for automated outreach.

On your own lead forms, the consent disclosure needs to be clear and conspicuous, placed immediately next to the submission button, and not buried in a general terms-of-service checkbox. The borrower has to be told they are consenting to automated calls and texts, from your company, at the number they provided. That language lives in the form. The timestamp, IP address, and form version live in your database.

Quiet-Hours Enforcement

Federal TCPA restricts calls and texts to between 8 a.m. and 9 p.m. in the recipient's local time zone. Not your time zone. The recipient's. Massachusetts operates on Eastern Time. A borrower in Phoenix runs Mountain Standard Time year-round because Arizona does not observe daylight saving. A borrower in rural Indiana might be on Eastern or Central depending on the county. Your automation cannot assume the area code maps cleanly to a single time zone.

The correct approach is to store a resolved IANA time zone identifier on the contact record at lead creation time, not just the phone area code. You resolve it by taking the ZIP code from the lead form, looking it up against a ZIP-to-timezone mapping, and writing the IANA string ("America/Phoenix", "America/Chicago") to a dedicated field. Every send-window check then reads that field, not the area code.

Opt-Out Processing

When a borrower replies STOP, or any standard opt-out keyword (STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT), your system must halt all future automated messages to that number immediately. Not at the next scheduled review. Immediately. If your sequences are queued and you are not checking a consent flag at dequeue time, a queued message can fire after an opt-out and that is a willful violation.

State "mini-TCPA" laws layer additional requirements on top. Florida, for example, has restrictions that go beyond federal quiet hours. Oklahoma and Washington have their own consent language requirements. If you are originating loans in multiple states, the compliance surface is larger than the federal floor suggests.

The Technical Build: How I Structured a TCPA Compliant Mortgage Automation System

Here is the sequence from lead capture to first touchpoint, with the compliance checkpoints embedded.

Lead Form and Consent Record Creation

The lead form collects: name, email, phone, ZIP code, and the consent checkbox. The checkbox is not pre-checked. The label text reads something like: "By submitting this form, you agree that [Company Name] may contact you by phone or text message, including automated messages, at the number provided. Consent is not required to obtain a loan." That last sentence matters. The FCC has held that conditioning a loan on consent is itself a violation.

On form submission, a webhook fires to the CRM or automation platform. The payload includes the phone number, ZIP code, consent boolean, a UTC timestamp, the form URL, and the submitter's IP address. The receiving endpoint writes all of that to a consent_records table with a foreign key to the contact. The IANA time zone is resolved from the ZIP at this step and written to the contact record.

I use a separate consent_records table rather than a field on the contact record because I want a full audit log. If a contact resubmits a form, that is a second row in consent_records, not an overwrite. The most recent valid consent record is what the send logic reads, but the history is preserved. Retention of those records should be at minimum four years, which aligns with the general TCPA statute of limitations.

The Sequence Queue and Send-Window Gate

When a new qualified lead lands in the CRM, the automation enrolls them in a follow-up sequence. The sequence is a series of scheduled touchpoints: an immediate SMS acknowledgment, a follow-up call attempt at a specified interval, an email drip, and so on. Each touchpoint is written to a send_queue table with a status of "pending" and a not_before timestamp derived from when enrollment happened.

A worker process polls the send_queue for pending items whose not_before time has passed. Before firing any touchpoint, the worker runs four checks in order:

  1. Consent check. Is there a valid, non-revoked consent record for this contact's phone number? If not, the item is skipped and flagged for human review.
  2. Opt-out check. Is the contact's opt_out flag set to true? If yes, the item is permanently cancelled and the contact is removed from all active sequences.
  3. DNC check. Has the number been scrubbed against the National DNC Registry and any active litigator-scrub list within the last 30 days? If not, hold the item and trigger a scrub job.
  4. Send-window check. Is the current UTC time within the 8 a.m. to 9 p.m. window in the contact's stored IANA time zone? If not, the item stays pending and the worker picks it up again on the next poll cycle.

Only an item that passes all four checks gets fired. This is not elegant but it is correct. Every check is logged with a result and a timestamp so there is an audit trail for every send decision, including the ones that were blocked.

Opt-Out Webhook: The Most Important Piece

When a borrower texts STOP to your SMS number, your SMS provider (Twilio, Bandwidth, or whoever) fires an inbound webhook to your system. That webhook handler must do three things before it returns a 200:

  1. Set opt_out = true and opt_out_timestamp = now() on the contact record.
  2. Cancel all pending items in send_queue for that contact's phone number, setting their status to "cancelled_optout".
  3. Write a row to the consent_records table recording the revocation, including the inbound message text and timestamp.

The reason the worker also checks the opt-out flag at dequeue time, even though the webhook should have already cancelled the queued items, is defense in depth. Race conditions exist. If a message was in the middle of being processed when the opt-out came in, the dequeue check catches it. You want two walls, not one.

Non-standard opt-out language is a real problem. Borrowers text "stop texting me," "remove me," "take me off your list," and variations that do not match the standard keyword list. Your inbound message handler should run a simple intent classifier on any inbound message that is not a standard keyword. It does not need to be complex. A short list of patterns and a fallback to human review is enough. I have flagged this to human review on about 3 percent of inbound replies in production, which is manageable.

TCR Registration and AI Disclosure

If you are sending SMS at any meaningful volume, you need to register your brand and campaigns with The Campaign Registry (TCR). Carriers use TCR registration to determine whether to deliver your messages. Unregistered 10-digit long code (10DLC) traffic gets filtered aggressively. Registration requires a brand profile (your EIN, company name, website) and a campaign use case that describes the message content. Mortgage follow-up is a standard use case. Plan for a few days of processing time and a small per-campaign monthly fee.

On AI disclosure: if any part of your outreach is driven by an AI agent or chatbot that a borrower might interact with conversationally, several state laws now require you to disclose that the contact is not a human. California's BOT Disclosure Act is the most cited example, but similar requirements are spreading. I build the disclosure into the opening message of any AI-assisted interaction: something like "Hi, this is an automated assistant from [Company]. A loan officer will follow up personally." Short, clear, not buried.

What I'd Actually Do: The Tradeoffs I Accepted

I could have bought a compliance-forward mortgage CRM that handles all of this in its UI. Several exist. I did not, for two reasons. First, the black-box compliance layer in most vendor platforms does not give me visibility into what is actually being checked and when. If a violation happens, I cannot reconstruct the decision log without going back to the vendor. Second, the customization ceiling on those platforms is real. The consent-record schema I described above is not something you can easily build inside a packaged CRM's data model.

What I gave up is speed of initial setup. Rolling your own compliance layer takes longer than clicking through a vendor wizard. The upside is that I own the audit trail, I know exactly what every check does, and I can extend it when a new state law changes the requirements. The TCPA landscape is not static. Florida updated its rules, the FCC's one-to-one consent order changed the lead-buying calculus, and more state AI-disclosure laws are coming. Owning the build means I can adapt without waiting on a vendor's roadmap.

The one place I would not try to build from scratch is DNC scrubbing. The National DNC Registry API and the litigator-scrub databases require their own integrations, and the litigator-scrub vendors update their lists continuously. I use a third-party service for that specific check and call it as part of the pre-send gate. That is a clean division: I own the consent and opt-out logic, a specialist service owns the list scrubbing.

If you are running fewer than a few hundred leads a month, this level of architecture is probably over-engineered for where you are. A compliant CRM with solid consent collection, standard opt-out handling, and quiet-hours settings turned on will cover most of the risk surface. If you are scaling beyond that, or if you are buying leads from aggregators and need to verify consent chain, building this yourself is worth the investment.

For the structured output patterns that power the consent classification and intent detection pieces of this system, the approach I use is described in my post on forcing structured output from the Claude API. The same validation logic applies: you define a schema, the model fills it, and you reject anything that does not parse.

What Actually Breaks in Production

A few things have gone wrong that are worth naming.

ZIP-to-timezone resolution fails on P.O. Box ZIPs. Some leads fill in a P.O. Box ZIP rather than a residential ZIP. The mapping tables I was using did not always return a reliable time zone for those. The fix was to fall back to the area code's most common time zone when the ZIP lookup returns null, and flag the record for manual verification. It is imprecise but it is better than sending at a wrong hour because the field was null.

Webhook delivery failures from the SMS provider. If the inbound opt-out webhook fails to reach your endpoint (your server is down, there is a timeout, the URL changed), the opt-out does not get processed. Twilio and similar providers retry on failure, but you should also build a reconciliation job that pulls the inbound message log from the provider's API on a schedule and checks for unprocessed opt-out keywords. Belt and suspenders.

Consent record orphans after CRM contact merges. When a contact is merged in the CRM, the foreign key on the consent_records table can end up pointing at the archived contact ID. After a merge event, a cleanup job needs to re-link consent records to the surviving contact. This is a data hygiene problem that only surfaces when you start querying consent records for a contact and get zero rows back for someone who definitely submitted a form.

If you originate mortgages and want to talk through how this kind of automation applies to your specific pipeline, I am licensed in Massachusetts and work with borrowers directly at NewFed. You can start an application here and we can figure out what your process actually needs.