Project Webhooks

Signed project lifecycle notifications, payloads, verification and retries.

Configuration

Property administrators: Settings → Integrations → Webhooks. Enter a public HTTPS destination, enable project updates, then Save changes.

Copy the signing secret when first shown. Refreshing or leaving the page hides it. Generate new secret opens a confirmation and rotates immediately; it does not save other form edits. New attempts use the current secret. During rotation, briefly accept both old and new secrets for in-flight deliveries.

One destination per property. URLs must use HTTPS on port 443, be at most 2048 characters, and have no credentials or fragment. Private/local addresses and redirects are blocked. Paths and query strings are allowed.

Delivery contract

POST with Content-Type: application/json, User-Agent: MatchiFi-Project-Webhooks/1, X-MatchiFi-Event-ID and X-MatchiFi-Signature.

Body fieldMeaning
idStable UUID. Deduplicate by this ID.
type / versionproject.updated / 1.
occurredAtSnapshot creation time, not the lender decision time.
projectIDMatchiFi Project ID, also data.shortID.
externalReferenceProject reference or null.
changesOne or more lifecycle change labels.
dataProject snapshot: identity, reference, merchant, author, addresses, users and applications.
{
  "id": "11111111-1111-4111-8111-111111111111",
  "type": "project.updated",
  "version": 1,
  "occurredAt": "2026-10-01T12:00:00.000Z",
  "projectID": 300001,
  "externalReference": "JOB-0042",
  "changes": [
    "lender.matched"
  ],
  "data": {
    "shortID": 300001,
    "externalReference": "JOB-0042",
    "partner": null,
    "merchant": {
      "appID": "EXAMPLEHVAC",
      "name": "Example Heating"
    },
    "author": null,
    "address": null,
    "users": [],
    "prequalifications": [
      {
        "id": 400001,
        "stage": "pending",
        "lender": "ftl",
        "lenderID": null,
        "expiresOn": null,
        "cancelledOn": null,
        "submittedOn": null,
        "approvedOn": null,
        "declinedOn": null,
        "contractSignedOn": null,
        "installationCompletedOn": null,
        "completionCertificateCreatedOn": null,
        "completionCertificateSignedOn": null,
        "fundsReleasedOn": null,
        "fundedOn": null,
        "expired": false,
        "requestAmount": 8500,
        "approvedAmount": null,
        "amountFunded": null,
        "product": null,
        "service": "equipment",
        "equipment": null
      }
    ],
    "createdOn": "2026-10-01T11:00:00.000Z"
  }
}

Amounts in the snapshot are USD dollars. amountFunded is populated only for funded applications; unavailable lifecycle values are null. Each prequalifications[] item describes a separate application.

Milestones that trigger a webhook

A webhook is sent when a lender application reaches or changes one of these milestones:

  • Lender matched or no match found
  • Application submitted
  • Application approved or declined
  • Contract signed
  • Installation completed
  • Completion certificate created or signed
  • Funds released
  • Funding completed
  • Application canceled
  • Application expired

Corrections or reversals to tracked milestones can also send a project.updated notification.

Each notification includes the current Project snapshot and labels identifying what changed. One notification may include multiple milestones.

Supported change labels: lender.matched, lender.no_match, application.started, application.approved, application.declined, contract.signed, installation.completed, completion_certificate.created, completion_certificate.signed, funds.released, funding.completed, application.canceled, application.expired, project.updated.

Signature verification

X-MatchiFi-Signature is t=<Unix seconds>,v1=<hex digest>. Verify HMAC-SHA256 of timestamp + "." + rawBody before parsing JSON. Use the displayed secret string as the key; do not hex-decode it. Compare in constant time and reject stale delivery timestamps. This example allows five minutes of clock skew:

import { createHmac, timingSafeEqual } from "node:crypto"

function _verifyMatchiFiSignature(rawBody, header, secrets) {
  const match = typeof header === "string" && /^t=([1-9][0-9]*),v1=([a-f0-9]{64})$/.exec(header)
  if (!Buffer.isBuffer(rawBody) || !match) return false
  const timestamp = Number(match[1])
  if (!Number.isSafeInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false
  const received = Buffer.from(match[2], "hex")
  return secrets.some(secret => {
    const expected = createHmac("sha256", secret).update(match[1] + ".").update(rawBody).digest()
    return received.length === expected.length && timingSafeEqual(received, expected)
  })
}

After verification, validate the body and confirm the event-ID header matches body.id. Return 2xx after durably accepting the event, including duplicates. Keep processing short: HTTP requests time out after eight seconds.

Retries

Network errors, timeouts, HTTP 408, 425, 429 and 5xx retry automatically. Other non-2xx statuses fail, including redirects and 410. Unsafe destinations fail without sending.

Up to eight attempts per cycle. Retry delays after failures 1–7 are 1 minute, 5 minutes, 15 minutes, 1 hour, 4 hours, 12 hours and 24 hours. Retry-After can extend a delay, up to 24 hours. The scheduled worker and backlog can add latency.

Retries keep the same ID, exact body and occurredAt, with a fresh delivery signature. There is no sequence counter or ordering guarantee. Process deliveries idempotently and avoid replacing newer state with a late retry.

Configuration changes and coverage

Enabling does not backfill history. New app-driven lifecycle updates can notify for existing Projects. Expiration is checked separately from the last activation/check; older or backdated expirations are not retroactively scanned. Ordinary field edits and direct/bulk database changes do not themselves emit events. Queue failures never fail the lender update, but can leave a notification gap.

Disabling stops new queueing; queued events are canceled when the worker observes the disabled setting. In-flight requests can finish. Re-enabling does not replay canceled events, although still-pending events may remain eligible.

Changing the URL does not forward old queued events automatically: they fail with callback_changed. New events use the new URL. The signing secret stays unchanged unless explicitly rotated.

Payloads can contain saved homeowner names, contact details and addresses. Protect stored payloads. SSNs and raw lender payloads are excluded.