Inspiration

Before a loan closes, a lease is signed, or an offer is confirmed, somebody has to prove the applicant works where they claim.

In the United States a payroll database answers for large employers. The Work Number holds 823 million employee records from roughly 5 million employers. For everybody else it is a phone call, and the industry's own description of that fallback is blunt: it "takes days or weeks and often fails entirely when the employer does not respond." Equifax sells manual verification as a paid waterfall product for exactly this case. Its implementation is a human on hold.

Outside the US there is no equivalent database at all, so verification is manual by default across the EU, UK, Canada, Australia, India, Southeast Asia and Latin America. Global employment screening was USD 5.3 to 8.0 billion in 2026, heading toward roughly USD 13.7 billion by 2034.

Then there is the part nobody checks. Half of mortgage fraud findings involve potentially fraudulent income or employment. Fake employers are not crude: investigators describe them as having legitimate phone numbers, automated call centres, real addresses and directory listings. Fannie Mae's red flags include "employer unable to be contacted".

The employer phone number on an application is the single field a fraudulent applicant fully controls. The manual process dials it anyway. That is the gap Certa was built for.

What it does

Certa runs verification-of-employment calls from an Airtable view the operator already uses. It calls only employers the applicant consented to, asks a bounded set of three questions, and writes a typed answer back into their own columns.

The applicant's number is structurally undialable. This is a type relationship, not a validation rule.

class NumberSource(str, Enum):
    OFFICIAL_SITE = "official_site"
    BUSINESS_REGISTRY = "business_registry"
    DIRECTORY = "directory"
    KNOWN_EMPLOYER_RECORD = "known_employer_record"
    # deliberately no APPLICATION member

@dataclass(frozen=True, slots=True)
class ApplicantSuppliedNumber:
    raw: str   # terminal type: no conversion to SourcedNumber exists

The dialer's only parameter type carries a SourcedNumber tagged with independent provenance. The number written on the application is a separate type with no conversion path. No line of code turns one into the other, so it cannot be forgotten at a call site.

A payroll database cannot catch a fake employer, because a fake employer is by construction not in it. This is a control neither existing method performs.

The extraction schema is derived from your own columns. Nobody writes JSON Schema. They create columns.

single select "Employment confirmed"  (Yes | No | Unknown)
  description: "Use yes only when the employer states the person currently works there."

  ->  { "type": "string",
        "enum": ["yes", "no", "unknown"],
        "description": "Use yes only when the employer states..." }

A single select's choices are the enum, its description is the extraction instruction, its name is the field. Add a question by adding a column.

Consent is a hash, not a checkbox.

token = sha256(request_id | phone | relationship | task_spec_version | consent_receipt_id)

Compared with hmac.compare_digest. A token dragged down a column was derived from a different number, so it fails. Spreadsheet drag-fill is the most ordinary way consent gets faked, and it simply does not work here. Editing what the agent may say bumps TASK_SPEC_VERSION, invalidating every consent gathered under the previous wording: consent to be asked three questions is not consent to be asked a fourth.

How we built it

Python 3.11+, standard library only. No install, no build step, no lockfile, zero dependencies. A loopback-only console, so the operator never touches a terminal. With no credentials it opens on bundled sample data and places no calls.

Surface Role
CALL-E Developer API (REST) Primary. The only surface accepting result_schema and recipient_result_schema
CALL-E MCP via the calle CLI Automatic fallback. Takes no schema, so it can never read as verified
Airtable Web API v0 Rows, field definitions, writeback

Ordered fail-closed gates turn a call into a disposition, a hash-chained append-only audit log records every consent and dispatch, and records are fsynced before the call they authorise goes out: a record with no call is recoverable, a call with no record is not.

274 tests, standard-library unittest, no third-party runner.

Challenges we ran into

CALL-E issue #341. The API can return task_completed: true when voice-agent setup fails before dialing. For a verification product that means employment confirmed with no conversation. Nothing reaches a positive disposition without reached_employer: yes extracted from the call, and a table whose columns cannot express that gate is refused at setup.

A live call exposed a bug no test could. The call connected, the agent did its job, CALL-E extracted correctly, and the writeback failed:

Airtable 422 INVALID_MULTIPLE_CHOICE_OPTIONS
Insufficient permissions to create new select option "unknown"

We appended unknown to every derived enum, per CALL-E's own guidance, while our base template created those columns as Yes/No. The first genuinely unclear answer turned a paid call into a failed write. Airtable's Update Field API can change a field's name and description but not its choices, so the fix could not be automatic. certa/checkup.py now reports every column that cannot hold an answer, names the exact change, and refuses to plan a run until it is made.

Dialing without a region. A live +91 call was accepted, dialed, and refused by the carrier in zero seconds with a bare SIP 404. build_call_payload accepted a region; the runner never passed one. The region is now derived from the number's own country code, and destinations outside CALL-E's published 42-country coverage are skipped by name before a call is spent.

Accomplishments that we're proud of

Merged into CALL-E's own repository during the hackathon. plugins/airtable-calle-certa now lives on main.

A real call, verified end to end: 51 seconds, 13 turns, confidence 0.93.

bot:  automated employment verification call on behalf of <requester>,
      regarding <applicant>, who has given written consent for this contact
hr:   Yes, he works here. Operations manager. Started March last year.
bot:  Just to make sure I have that right: Operations Manager, started March.

  reached_employer: yes . employment_confirmed: yes  ->  verified

The row wrote back as verified with its call id, consent receipt and reason, and the hash-chained audit log verified intact across consent, authorisation, dispatch and interpretation.

And a README that states what has not been demonstrated. Later attempts failed inside CALL-E's telephony leg before the destination rang, with failure codes varying across byte-identical payloads. It is reported upstream and recorded in the repository rather than omitted.

What we learned

The hard part of an agent placing real phone calls is not the call. It is deciding who may be called, what may be asked, and whether the answer counts, and all three have to be code, not a prompt.

Certa runs no model of its own. CALL-E's model speaks, listens and extracts; every trust decision is deterministic. If Certa did the extraction, a hallucination would become a verified employment record in a lending file.

We also learned to distrust our own conclusions. Twice during the build a diagnosis looked settled and was wrong, and both corrections are recorded in the commit messages rather than quietly dropped.

What's next for Certa: Employment Verification That Refuses the Form

Reference checks and tenancy verification are the same shape: a third party who did not ask to be called, a consent that must be provable, bounded questions, and an answer that has to be typed. The consent-token and provenance-type machinery is not employment-specific.

Since submitting, review on the pull request surfaced two ways a result could reach "verified" without actually being checked. A call carrying no completion_confidence skipped the floor comparison entirely. And a value outside the derived schema, such as "probably" or "YES", was neither a contradiction nor an unknown, so it counted as an established fact. Both now route to human review, with tests that fail if either regresses.

The plugin was merged into CALL-E's repository during the hackathon (PR #552), after a maintainer review round listing four Must Fix items. All four were addressed within the hour, and the maintainer ran the suite credential-free before merging.

Built With

Share this project:

Updates

Submission history