Webhooks
Webhooks push events from MCA Manager into your own systems in real time. Manage endpoints under Settings → Webhooks (requires the edit company settings permission).
Add an endpoint
Provide the destination URL and the events you want (or * for all).
The URL must be a public https address; private networks, localhost and
cloud metadata addresses are refused, and the address is checked again before
every delivery. Each endpoint gets a signing secret, shown once, used to sign
every delivery.
Events
| Event | When |
|---|---|
deal.created | A deal was created (staff, API, portal or email intake). |
deal.updated | A deal's fields were edited (staff or API). changed names the fields; values are not repeated — fetch the deal. |
deal.deleted | A deal was deleted (only possible while nothing references it). A tombstone for updatedSince sync. |
deal.state_changed | A deal moved to a new state (funded, collections, closed, defaulted…). |
merchant.created | A merchant record was created. |
lead.created | A lead was created. |
lead.status_changed | A lead's status changed. |
submission.status_changed | A funder submission moved to a new pipeline status. |
payment.created | A payment row was created: a pull scheduled, a fee charged, a payment recorded or imported (rows from history loads only when recent). A schedule rebuild sends one per new pull. |
payment.cancelled | A payment was cancelled (by staff, a payoff close-out, a schedule repair or rebuild). Kept on record; never collected. |
payment.deleted | A mistaken manual payment was deleted (its journal reversed). A tombstone for updatedSince sync. |
payment.refunded | Money went back to the merchant (a refund was paid out); also sent as payment.paid with a negative credit amount. |
payment.paid | A payment was marked paid (staff, processor, import or API). type and direction tell a collection from a fee or a refund (a refund is a negative credit). |
payment.bounced | A payment was returned; carries the NACHA return code when known. |
merchant.bank_account_changed | A merchant's bank account was added, edited, re-ordered or removed. Never carries account or routing numbers. |
person.merged | Two person records were merged; contacts on the dropped one now point at the kept one. |
contract.generated | A contract requested through the API (POST /deals/{id}/contracts) was generated: a draft signature request is ready to review and send. |
contract.failed | A contract requested through the API could not be generated; carries the reason (no signers chosen, template archived, conversion failed…). |
esign.sent | A signature request went out to its signers (staff or API). |
esign.voided | A signature request was withdrawn; its signer links stop working. |
esign.signed | One party signed a document sent for signature. |
esign.completed | Every party signed; carries the filed document id and url. |
esign.declined | A signer declined; the request stopped and carries their reason. |
webhook.test | Sent by the Send test button. |
The same list, with example payloads, is returned by GET /api/v1/webhook-endpoints
and published in the API description (/api/openapi.json, under webhooks).
Where an event came from
Payment and deal events carry a source in data:
| Source | Meaning |
|---|---|
staff | Someone on a screen in MCA Manager. |
api | A call to the REST API (an API key or a connected app). |
processor | A payment processor's callback or status poll (ACH settled, returned). |
automation | A rule or scheduled job (auto-default, schedule escalation, close-out). |
portal | A merchant, ISO or syndicator portal. |
intake | Email intake or the hosted application form. |
payoff | An early payoff or renewal payoff. |
import | Settings → Import. Imported rows send events only when they are recent (the last 7 days) and the import's Send webhooks box was left on. |
Envelope
{
"id": "evt_01JAX8Q4T6V2",
"event": "payment.paid",
"version": 1,
"occurredAt": "2026-09-22T14:03:11.000Z",
"data": {
"paymentId": 9001,
"dealId": 1042,
"amount": "425.00",
"source": "processor"
}
}
id is the event id. It is the same for every endpoint the event goes to and
for every redelivery of it, so it is what to de-duplicate on: keep the ids you
have processed and skip a repeat. version is bumped only for breaking changes
to the envelope or a payload.
Headers repeat it: X-Webhook-Event-Id (the event id above), X-Webhook-Event
(the event name) and X-Webhook-Id (this one delivery attempt's row, whd_…,
handy when asking support about a specific delivery). Deliveries are never
redirected: a 3xx from your receiver counts as a failure.
Verifying signatures
Each delivery is a JSON POST with headers:
X-Timestamp- unix-style timestamp (seconds)X-Signature- HMAC-SHA256, hex
Compute the signature over the string timestamp + "." + body using your
endpoint’s secret and compare with X-Signature in constant time. Reject if they
don’t match or the timestamp is more than five minutes old.
Rotating the secret: Rotate secret on an endpoint (or POST /api/v1/webhook-endpoints/{id}?action=rotate) issues a new secret. For 24 hours
every delivery also carries X-Signature-Previous, signed with the old secret, so
you can switch at your own pace.
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = createHmac("sha256", SECRET).update(`${ts}.${rawBody}`).digest("hex");
const ok = expected.length === sig.length && timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
import hmac, hashlib, time
def verify(secret: str, ts: str, raw_body: bytes, sig: str) -> bool:
if abs(time.time() - int(ts)) > 300: return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
Delivery, retries and the log
Deliveries are at-least-once. Each attempt times out after 10 seconds; a failed attempt is retried five times over roughly eight minutes (15 s, 30 s, 1 m, 2 m, 4 m). Return any 2xx quickly and do the work afterwards.
Open Deliveries on an endpoint to see every attempt: time, event, status, HTTP response, tries, duration, the first part of your response body, and the payload. Redeliver sends a past payload again as a new delivery. Rows are kept for 30 days.
An endpoint that fails 20 deliveries in a row is paused and the admins are notified; fix the receiver, then Re-enable it. Turning an endpoint off yourself is not a pause and is never notified.
Managing endpoints through the API
With an API key that carries the Webhooks API scope:
GET/POST /api/v1/webhook-endpoints, GET/PATCH/DELETE /api/v1/webhook-endpoints/{id},
POST /api/v1/webhook-endpoints/{id}?action=rotate. The list call also returns the
event catalogue with example payloads.
Inbound webhooks
POST /api/webhooks/inbound/<source> accepts signed payloads from your systems
(bank statements, credit profiles, disclosures, legal updates) using the same
HMAC scheme with the inboundWebhookSecret setting. Send an eventId (or id)
with each payload: a repeat of the same id within 30 days is acknowledged and not
applied again.
Can’t find what you need? Return to the app or contact your administrator.