APISign in

EngageCycle webhooks

EngageCycle sends an event to your webhook each time something happens to a proposal, a document, a walkthrough, an offer's prospect, or an email: a proposal sent, viewed, signed, or declined, a prospect who confirmed their email, and so on. Your tool (n8n, Zapier, Make, a CRM, or your own code) receives each one as an HTTPS POST with a JSON body, and checks its signature with the endpoint's signing secret.

Every event is also recorded, whether or not any endpoint takes it, and you can read them with GET /api/v1/events instead of (or as well as) a webhook. See Polling instead.

Endpoints

An owner adds endpoints in Integrations, under Webhooks. Each endpoint has its own address, signing secret, on and off switch, and choice of events. Your plan sets how many endpoints you can have.

  • Which events. An endpoint takes every event, or only the events ticked. "Every event" includes kinds added later, but never documents' events (document.*) or walkthroughs' events (packet.*): an endpoint gets those only when they're ticked. So an automation built on proposals (filing each proposal.signed in your practice software, say) is never reached by an agreement or a consent by accident.
  • webhook.test is sent only by Send test event, and only to that one endpoint, whatever events it takes.
  • A new endpoint starts off. Turning it on can also send it the last 7 days of events it would have had, oldest first.
  • The signing secret is shown once, when the endpoint is made (and again each time you make a new one with New secret, which stops the old one at once).

Each endpoint that's on and takes an event's type gets its own delivery of it, signed with its own secret, retried and ordered on its own. The event's id is the same at every endpoint.

Headers

POST /your/webhook/path HTTP/1.1
Content-Type: application/json
User-Agent: EngageCycle-Webhooks/1.0
X-EngageCycle-Event: proposal.signed
X-EngageCycle-Event-Id: evt_6e2f0c1a9b8d4e7f8a1b2c3d4e5f6a7b
X-EngageCycle-Timestamp: 1799679600
X-EngageCycle-Signature: v1=5d41402abc4b2a76b9719d911017c592ae2f5c8e1d6c4f0b7a3e9d8c2b1a0f9e
Header What it is
X-EngageCycle-Event The event type, the same as the body's type.
X-EngageCycle-Event-Id The event's id, the same as the body's id: evt_ and 32 hex characters. It never changes across retries, and it's the same at every endpoint, so use it to ignore an event you already handled.
X-EngageCycle-Timestamp When this try was sent, in Unix seconds. Each try has its own.
X-EngageCycle-Signature v1= and the hex HMAC-SHA256 of the timestamp, a period, and the raw body, keyed with the endpoint's signing secret.

Check the signature

Before you trust an event, check that it came from EngageCycle and wasn't changed or replayed:

  1. Take the raw body exactly as it arrived, before any JSON parsing. Parsing and re-serializing it changes the bytes and the signature won't match.
  2. Compute HMAC-SHA256 of {timestamp}.{raw body} (the X-EngageCycle-Timestamp value, a period, then the body) with the endpoint's signing secret, as lowercase hex, and put v1= in front.
  3. Compare it with X-EngageCycle-Signature in constant time.
  4. Reject the event when the timestamp is more than 5 minutes (300 seconds) from your clock, either way.

In Node:

const crypto = require("node:crypto");

/** True when the event is from EngageCycle, unchanged, and recent. */
function verifyEngageCycle(secret, headers, rawBody) {
  const timestamp = Number(headers["x-engagecycle-timestamp"]);
  const given = String(headers["x-engagecycle-signature"] ?? "");
  if (!Number.isInteger(timestamp) || !given.startsWith("v1=")) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(given.slice(3), "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && a.length > 0 && crypto.timingSafeEqual(a, b);
}

With Express, read the body raw for this route, check it, then parse it:

const express = require("express");
const app = express();

app.post("/engagecycle", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyEngageCycle(process.env.ENGAGECYCLE_SECRET, req.headers, raw)) {
    return res.status(401).end();
  }
  const event = JSON.parse(raw);
  // Ignore an event you already handled: event.id is the same on every try.
  res.status(204).end();
  // Then act on event.type and event.data.
});

In n8n, a Webhook node with Raw Body on gives you the bytes to check in a Code node before anything else runs.

Delivery and retries

  • Answer with any 2xx within 10 seconds. That marks the event delivered. Anything else, no answer within 10 seconds, or a redirect (redirects are never followed) counts as a failed try. Answer first and do slow work afterwards.
  • Failed tries are retried 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours after each failure, then every 12 hours, until the event is 7 days old. Then it's marked failed. Owners get a notice when an endpoint has failed three times in a row.
  • Send it again. Resend in Integrations, Delivery log, or POST /api/v1/events/{id}/redeliver, makes a delivered or failed event due again at that endpoint from its first try.
  • The address must be a public https address.
  • When your plan stops including an endpoint, its deliveries wait, without counting a try, until the plan includes it again.

Order

Events for different proposals can arrive in any order. For one proposal or document, each endpoint gets its events in the order they happened: the next one waits until the one before it was delivered there or given up on. Within one walkthrough the same holds: packet.sent, then its documents' events, then packet.completed.

Some events wait for a file:

  • proposal.signed and document.signed are sent once the signed PDF is ready, so their pdf block is always filled in.
  • packet.completed is sent once every signed step's PDF is ready.

When two events are about two versions of one proposal (a revision's proposal.sent and the old version's proposal.superseded), link them by id rather than by when they arrived.

Some events carry a link for your automation to send the client: signing_url, each signer's signing_url, copied people's view_url, a walkthrough's packet_url, and an abandoned prospect's resume_url. EngageCycle never stores these links in the event. Each one is put in when the event is delivered, and only while it still works. A link that no longer works (the proposal was signed, declined, withdrawn, replaced, or expired, or a newer link was sent) arrives as null, for example on a retry after an outage or a Resend days later.

The body

Every event has the same envelope:

{
  "id": "evt_6e2f0c1a9b8d4e7f8a1b2c3d4e5f6a7b",
  "type": "proposal.signed",
  "created_at": "2027-01-18T15:00:00.000Z",
  "tenant": { "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d", "slug": "example-office" },
  "data": { }
}
  • id: the event's id, as in X-EngageCycle-Event-Id.
  • type: what happened. The list is below.
  • created_at: when it happened, in UTC (ISO 8601).
  • tenant: your business, by its id and its address name.
  • data: the event itself. Its shape depends on type.

Ids are UUIDs. Money is in integer minor units (cents for USD) with the business's currency code beside it. Times are UTC. The order of keys in the JSON isn't meaningful and can differ from the examples. New keys can be added to an event at any time, so ignore keys you don't know.

In the examples below every id, link, and hash is made up, and the people and businesses are fake. They're made from what EngageCycle really sends: each time EngageCycle changes, the examples are made again from it.

Choosing what to listen to

You want to Listen to
Email the client their link from your own tool proposal.sent (and document.sent)
File a signed proposal in your practice software or CRM proposal.signed
Create a new client from an offer in your CRM client.created (not lead.verified, so prospects who never sign aren't created)
Follow up with prospects who didn't finish lead.abandoned
Follow up before a deadline read expires_at on any proposal event, and listen to proposal.deadline_extended
Stop emailing an address that bounced email.bounced, email.complained

The proposal block

Every proposal.* event (and link.requested, lead.verified, lead.abandoned, and client.created when they're about a proposal) carries the proposal as data.proposal:

  • id, version: the proposal, and its version (1, then one more with each revision).
  • campaign: { id, name, type, period } when it came from a campaign, or null.
  • client: who it's for: external_id (your own id for them, or null), display_name, email, flags (the client's information fields as the proposal used them), is_new, source, referral_partner, and utm. A business client also has type: "business", contact_name, and contact_title (either can be null); an individual's client has no type key at all. These are frozen when the proposal is sent.
  • origin: where it came from: { "type": "campaign", "id" }, { "type": "offer", "id", "slug" }, or { "type": "adhoc", "id": null } for one sent on its own.
  • status: sent, viewed, signed, declined, expired, revoked (withdrawn), or superseded.
  • visible_options: the ids of the packages the client could choose from.
  • booked: whether a call was booked.
  • expires_at: when the proposal really ends, after any extension. Read this one to follow up before the deadline.
  • original_expires_at: only on a proposal whose deadline was extended: the respond-by date its document printed. A proposal that was never extended has no such key.
  • supersedes: { id, version } of the version this one replaced, or null.
  • packet_id: only on a proposal that is a step of a walkthrough.

The document block

Agreements and consents sent on their own have their own events, document.*, never proposal.*, and carry data.document instead: the proposal block's keys without visible_options and booked, plus kind (agreement or consent) and template ({ id, name }). They go only to endpoints that tick them. There's no book-a-call on a document.

Proposal events

proposal.sent

A proposal is ready for the client: email them signing_url. EngageCycle emails it too when your business sends its own email. Written when staff choose Email it (or a key sends with mode: "link"), and again with is_resend: true when a link is sent again. Create link, copying a link, and signing in person write none. A proposal made from an offer writes none (the prospect is already holding the link; see lead.verified), and neither does a step of a walkthrough (see packet.sent).

{
  "id": "evt_cd940864700543e98771895f19c97eb4",
  "type": "proposal.sent",
  "created_at": "2027-01-11T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "signers": [
      {
        "name": "Jordan Example",
        "state": "open",
        "position": 1,
        "signing_url": "https://example-office.engagecycle.com/p/On7a9UWsOS6hYAXtuhNT8kp026sn-96xKrbvtS9l_bA"
      },
      {
        "name": "Taylor Example",
        "state": "open",
        "position": 2,
        "signing_url": "https://example-office.engagecycle.com/p/dVGzFqFIKXReyoYWrOiBFnS0utZa_I0yB1Grx9QJUVU"
      },
      {
        "name": "Morgan Example",
        "state": "open",
        "position": 3,
        "signing_url": "https://example-office.engagecycle.com/p/03_dKH7LXgzS3iPwHhEEr_3nvm3DTMGClZbV4P10KDQ"
      }
    ],
    "signing": {
      "rule": "any",
      "order": null
    },
    "proposal": {
      "id": "e80e9d66-947a-490d-bb61-5043472174f4",
      "booked": false,
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "sent",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-02T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ]
    },
    "is_resend": false,
    "copied_people": [
      {
        "name": "Casey Example",
        "view_url": "https://example-office.engagecycle.com/v/FV0Y0AgLQD-qq-f4hrUH0M-gmatjApEMde4PXdgjBRc"
      }
    ],
    "signing_url": "https://example-office.engagecycle.com/p/On7a9UWsOS6hYAXtuhNT8kp026sn-96xKrbvtS9l_bA"
  }
}

Only present sometimes, and shown above:

  • data.signing, data.signers: only present when the proposal has more than one signer.
  • data.proposal.client.type, data.proposal.client.contact_name, data.proposal.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).

copied_people lists the people copied on the proposal, each with their own view-only link (it can never sign), or []. With more than one signer, signing.rule is all or any (any one signer), signing.order is any or in_order (for all only), and each signer has their own signing_url (null while it isn't their turn, in order). The top-level signing_url is always the client's own.

proposal.viewed

The client opened the proposal for the first time. A copied person opening their copy never sends this.

{
  "id": "evt_8cd76000809848498c78cddc4f5e5dfe",
  "type": "proposal.viewed",
  "created_at": "2027-01-11T17:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "proposal": {
      "id": "e80e9d66-947a-490d-bb61-5043472174f4",
      "booked": false,
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "viewed",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-02T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ]
    },
    "viewed_at": "2027-01-11T17:00:00.000Z"
  }
}

Only present sometimes, and shown above:

  • data.proposal.client.type, data.proposal.client.contact_name, data.proposal.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).

proposal.copy_viewed

A person copied on the proposal opened their view-only copy for the first time. Once per person per proposal.

{
  "id": "evt_99d15ef5f48f4f34ad45bc7034989c8d",
  "type": "proposal.copy_viewed",
  "created_at": "2027-01-11T16:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "proposal": {
      "id": "e80e9d66-947a-490d-bb61-5043472174f4",
      "booked": false,
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "sent",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-02T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ]
    },
    "viewed_at": "2027-01-11T16:00:00.000Z",
    "copied_person": {
      "id": "d524d03c-b60a-486b-9a1e-98fe7ca6f5d6",
      "name": "Casey Example"
    }
  }
}

Only present sometimes, and shown above:

  • data.proposal.client.type, data.proposal.client.contact_name, data.proposal.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).

proposal.booking_clicked

The client clicked the book-a-call button. This is intent, not a confirmed booking.

{
  "id": "evt_20265e8be53f45c2b8509d15081d2349",
  "type": "proposal.booking_clicked",
  "created_at": "2027-01-11T18:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "proposal": {
      "id": "e80e9d66-947a-490d-bb61-5043472174f4",
      "booked": false,
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "viewed",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-02T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ]
    },
    "clicked_at": "2027-01-11T18:00:00.000Z"
  }
}

Only present sometimes, and shown above:

  • data.proposal.client.type, data.proposal.client.contact_name, data.proposal.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).

proposal.booked

A booking was confirmed for the proposal, through POST /api/v1/proposals/{id}/booked or POST /api/v1/bookings/match.

{
  "id": "evt_15d780857abc4fd29ebc9a8951a92ab8",
  "type": "proposal.booked",
  "created_at": "2027-01-12T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "booking": {
      "provider": "calendly",
      "starts_at": "2027-01-13T16:00:00.000Z",
      "external_id": "bk_1001"
    },
    "proposal": {
      "id": "e80e9d66-947a-490d-bb61-5043472174f4",
      "booked": true,
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "viewed",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-02T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ]
    }
  }
}

Only present sometimes, and shown above:

  • data.proposal.client.type, data.proposal.client.contact_name, data.proposal.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).

proposal.deadline_extended

Staff or an API key gave the client more time on the same proposal. The client's link keeps working, and the document keeps the respond-by date it printed. proposal.expires_at is the new deadline and original_expires_at the printed one. A proposal that had expired within the last 30 days reopens: deadline.reopened is true and proposal.status is sent or viewed again.

{
  "id": "evt_c7cc00f1e1bf44179ac3f0c58a090a04",
  "type": "proposal.deadline_extended",
  "created_at": "2027-01-16T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "deadline": {
      "to": "2027-02-16T05:59:59.999Z",
      "from": "2027-02-02T05:59:59.999Z",
      "reason": "Waiting on documents",
      "reopened": false,
      "extended_at": "2027-01-16T15:00:00.000Z",
      "extended_by": {
        "name": "Robin Example",
        "type": "user"
      }
    },
    "proposal": {
      "id": "e80e9d66-947a-490d-bb61-5043472174f4",
      "booked": true,
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "viewed",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-16T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ],
      "original_expires_at": "2027-02-02T05:59:59.999Z"
    },
    "notify_client": true,
    "signing_url": "https://example-office.engagecycle.com/p/uVmAdwggoiQVuxUcgpvbrYDixFjj2aXJ6ElB2Rc4MYc"
  }
}

Only present sometimes, and shown above:

  • data.proposal.client.type, data.proposal.client.contact_name, data.proposal.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).

deadline.from and deadline.to are the deadline before and after. extended_by.type is user or api_key (with the key's name). reason is your team's own note, never shown to the client, or null. When notify_client is true the client is emailed a second link to the same proposal (both work), and signing_url is that link; otherwise, or when it isn't the client's turn, it's null. Resending an expired proposal's link (within 30 days of its deadline) extends it first, with the reason "Sent a new link", then sends proposal.sent with is_resend: true. In a walkthrough, one is written for each open step, with packet_id and signing_url: null.

proposal.signer_signed

One of several signers signed. No email, IP address, or link is in it. chose is true for the signer whose choice of package and add-ons everyone agrees to (the first to sign). remaining lists who still has to sign; when it's empty, proposal.signed follows.

{
  "id": "evt_eda8b875d83249e18c8b568364a74377",
  "type": "proposal.signer_signed",
  "created_at": "2027-01-18T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "signer": {
      "name": "Jordan Example",
      "chose": true,
      "title": "Owner",
      "position": 1,
      "signed_at": "2027-01-18T15:00:00.000Z",
      "signing_method": "magic_link"
    },
    "proposal": {
      "id": "e80e9d66-947a-490d-bb61-5043472174f4",
      "booked": true,
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "viewed",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-16T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ],
      "original_expires_at": "2027-02-02T05:59:59.999Z"
    },
    "remaining": []
  }
}

Only present sometimes, and shown above:

  • data.signer.title: only present when the client is a business (the title this signer confirmed).
  • data.proposal.client.type, data.proposal.client.contact_name, data.proposal.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).
  • data.proposal.original_expires_at: only present when the deadline was extended (it is the respond-by date the document printed).

proposal.signer_declined

With any one signer, one signer declined while others can still sign. The proposal stays open. When the last one declines, proposal.declined is written instead.

{
  "id": "evt_36fdd537ef544dd59cd99ab81b1c38d5",
  "type": "proposal.signer_declined",
  "created_at": "2027-01-17T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "signer": {
      "name": "Taylor Example",
      "position": 2
    },
    "decline": {
      "note": "Too much for us this year.",
      "reason": "price",
      "reason_label": "The price"
    },
    "proposal": {
      "id": "e80e9d66-947a-490d-bb61-5043472174f4",
      "booked": true,
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "viewed",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-16T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ],
      "original_expires_at": "2027-02-02T05:59:59.999Z"
    }
  }
}

Only present sometimes, and shown above:

  • data.proposal.client.type, data.proposal.client.contact_name, data.proposal.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).
  • data.proposal.original_expires_at: only present when the deadline was extended (it is the respond-by date the document printed).

proposal.signed

The proposal is signed and its PDF is ready. This is the event to file a signed proposal from.

{
  "id": "evt_870bcb9c2b4644a08f7ad30d41e6db90",
  "type": "proposal.signed",
  "created_at": "2027-01-18T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "pdf": {
      "url": "https://app.engagecycle.com/api/v1/proposals/e80e9d66-947a-490d-bb61-5043472174f4/pdf",
      "sha256": "3f9f7bbf149b229534995e9ca8663f94bd52fab3c33bcbb2fe4aa89a51230dd5",
      "filename": "Proposal - Example Co - 2027.pdf"
    },
    "signers": [
      {
        "name": "Jordan Example",
        "chose": true,
        "title": "Owner",
        "position": 1,
        "signed_at": "2027-01-18T15:00:00.000Z",
        "signing_method": "magic_link",
        "signing_url": null
      }
    ],
    "proposal": {
      "id": "e80e9d66-947a-490d-bb61-5043472174f4",
      "booked": true,
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "signed",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-16T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ],
      "original_expires_at": "2027-02-02T05:59:59.999Z"
    },
    "signature": {
      "mode": "choose",
      "lines": [
        {
          "qty": 1,
          "name": "Annual with planning",
          "billing": "one_time",
          "unit_cents": 61000,
          "amount_cents": 61000,
          "catalog_item_id": null
        }
      ],
      "addons": [],
      "totals": [
        {
          "cents": 56000,
          "billing": "one_time"
        }
      ],
      "currency": "USD",
      "discounts": [
        {
          "id": "d1",
          "code": null,
          "kind": "per_client",
          "name": "Returning client discount",
          "label": "Returning client discount",
          "billing": "one_time",
          "trimmed": false,
          "percent_bp": null,
          "discount_id": null,
          "amount_cents": 5000
        }
      ],
      "documents": [],
      "signed_at": "2027-01-18T15:00:00.000Z",
      "selections": [
        {
          "group": "packages",
          "title": "Plus",
          "option": "plus",
          "price_cents": 61000
        }
      ],
      "typed_name": "Jordan Example",
      "attested_by": null,
      "total_cents": 56000,
      "on_behalf_of": "Example Co",
      "signer_title": "Owner",
      "via_proposal": null,
      "listed_totals": [
        {
          "cents": 61000,
          "billing": "one_time"
        }
      ],
      "monthly_cents": 0,
      "signing_method": "magic_link",
      "prior_price_cents": null
    },
    "entitlements": {},
    "could_also_sign": [
      "Taylor Example",
      "Morgan Example"
    ]
  }
}

Only present sometimes, and shown above:

  • data.signature.discounts, data.signature.listed_totals: only present when at least one discount line came off (totals are then net of the discounts, and listed_totals are before them).
  • data.signers: only present when the proposal had more than one signer (each one who signed).
  • data.could_also_sign: only present when the proposal had more than one signer and any one of them could sign (the others, including any who declined).
  • data.signature.signer_title, data.signature.on_behalf_of, data.signers[].title: only present when the client is a business (the signer's confirmed title, and the business they signed for).
  • data.proposal.client.type, data.proposal.client.contact_name, data.proposal.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).
  • data.proposal.original_expires_at: only present when the deadline was extended (it is the respond-by date the document printed).
  • What was signed for. signature.lines lists everything, item by item: each item of the chosen package (with its catalog item id, or null for one typed in), then each add-on taken. billing is one_time, yearly, semiannual, quarterly, monthly, or weekly. A line with a unit has unit_label and unit_plural ("form", "forms"). A rate billed as used has "as_used": true, its rate in unit_cents, and qty and amount_cents of 0, and is in no total.
  • Totals. signature.totals is what the client agreed to pay, one entry per billing frequency, in the order one_time, yearly, semiannual, quarterly, monthly, weekly, never added across frequencies. Read totals. total_cents (one-time plus yearly) and monthly_cents are kept only for older integrations.
  • Discounts. When a discount line came off, signature.discounts lists each one (amount_cents is taken off, in its billing), and listed_totals is the totals before them: listed_totals less discounts by billing equals totals. selections, addons, and lines keep listed prices. To push a signed proposal into another system, send each lines entry as a service and each discounts entry as a negative line in its billing.
  • Choices. selections are the packages chosen, addons the add-ons ticked. mode is fixed (one price to approve), choose (packages), or null for older proposals.
  • How it was signed. signing_method is how the signer signed (magic_link from their emailed link, offer_verified after confirming their email on an offer, or in_person). attested_by is the login id of the staff member who witnessed an in-person signing, or null. via_proposal is { id, version } when the client signed through an older version's link, or null.
  • Consent documents. signature.documents lists each consent document the proposal showed, as { id, title, version, required, agreed }.
  • entitlements is deprecated: it's empty for proposals made in the builder. Use signature.lines.
  • The PDF. pdf.url needs your API key (Authorization: Bearer). pdf.sha256 is the file's SHA-256, also sent as X-EngageCycle-Sha256 when you download it.
  • Several signers. signature is the agreement: the choice, and the typed name and method of the signer who chose. signed_at is when it became fully signed. signers lists each one who signed; its signing_url is always null by now.
  • A business client. signature.signer_title is the title the signer confirmed when signing (it can differ from contact_title) and on_behalf_of is the business's name. With several signers each entry of signers has its own title.
  • From an offer. proposal.origin.type is offer and proposal.client.is_new is true, so you can create the client in your CRM first.

proposal.declined

The client declined. reason is a stable id to branch on (each template sets its own reasons; the defaults are price, switching_provider, doing_it_myself, no_longer_needed, and other), and reason_label is the wording the client saw. With several signers, decline.signer is { position, name } of the one who declined (null when staff recorded it).

{
  "id": "evt_254ece4e28274a78a36acc2f957267c3",
  "type": "proposal.declined",
  "created_at": "2027-01-13T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "decline": {
      "note": "We found someone closer to home.",
      "reason": "switching_provider",
      "reason_label": "I'm going with someone else"
    },
    "proposal": {
      "id": "6ad29928-c32b-439a-874e-c600a32dec24",
      "booked": false,
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1002",
        "display_name": "Riley Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "declined",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-02T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ]
    }
  }
}

proposal.expired

The proposal reached its deadline unsigned.

{
  "id": "evt_43fa8ef668fa401c9bdf2eb1d3429baa",
  "type": "proposal.expired",
  "created_at": "2027-02-02T06:59:59.999Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "proposal": {
      "id": "19541151-95d7-44c9-9488-d3d5fa01819a",
      "booked": false,
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1003",
        "display_name": "Avery Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "expired",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-02T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ]
    },
    "expired_at": "2027-02-02T06:59:59.999Z"
  }
}

proposal.withdrawn

Staff withdrew the proposal. Its link now says so and it can't be signed. proposal.status is revoked. notify_client says whether staff chose to email the client (a business with its own email sender sends the notice; otherwise your automation can).

{
  "id": "evt_b612150f06e146dfb4ae52075494d94d",
  "type": "proposal.withdrawn",
  "created_at": "2027-01-14T17:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "proposal": {
      "id": "643ccda8-513c-4315-8e49-a33d208df121",
      "booked": false,
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1004",
        "display_name": "Quinn Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "revoked",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-02T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ]
    },
    "withdrawn_at": "2027-01-14T17:00:00.000Z",
    "notify_client": true
  }
}

proposal.superseded

Staff revised a sent proposal and sent the new version, which replaces this one. proposal is the old version (status superseded), and superseded_by is the new one, whose own proposal.sent is written at the same moment. same_recipient says whether the new version went to the same email address, in which case the client's old link leads to it. Revising a partly signed proposal voids the signatures given so far: everyone signs the new version.

{
  "id": "evt_142cc3d0a12d41d4b3c355b9dfaed4d5",
  "type": "proposal.superseded",
  "created_at": "2027-01-15T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "proposal": {
      "id": "d16f3201-3aa6-4931-82a8-e7819c516cdf",
      "booked": false,
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1005",
        "display_name": "Harper Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "superseded",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-02T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ]
    },
    "superseded_at": "2027-01-15T15:00:00.000Z",
    "superseded_by": {
      "id": "44fe80d7-47ad-4e84-bcca-431e8aaf829c",
      "version": 2
    },
    "same_recipient": true
  }
}

A client asked for their link again (on the page that emails a client their link) and a new link was issued. A proposal.sent with is_resend: true follows. Informational only. A client with no open proposal but an open document gets only a document.sent with is_resend: true. A client whose newest proposal has expired gets no event (the business's staff are told instead).

{
  "id": "evt_553697a00efc4a35a23d0edea811650e",
  "type": "link.requested",
  "created_at": "2027-01-14T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "proposal": {
      "id": "19541151-95d7-44c9-9488-d3d5fa01819a",
      "booked": false,
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1003",
        "display_name": "Avery Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "sent",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-02T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "std",
        "plus"
      ]
    },
    "requested_at": "2027-01-14T15:00:00.000Z"
  }
}

Document events

Agreements and consents sent on their own. Each has the same extra keys as its proposal counterpart above, with data.document in place of data.proposal, no price, and no copied people.

document.sent

{
  "id": "evt_6bf4f24d05b5421bac7f971bb5178ce4",
  "type": "document.sent",
  "created_at": "2027-01-19T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "signers": [
      {
        "name": "Jordan Example",
        "state": "open",
        "position": 1,
        "signing_url": "https://example-office.engagecycle.com/p/f9ZHBiiXNQGcbnhgONCYspvqFnSjoi4cJ7hPLzagu6s"
      },
      {
        "name": "Taylor Example",
        "state": "open",
        "position": 2,
        "signing_url": "https://example-office.engagecycle.com/p/OmwmOtkobUYjRZEtnLOSmxGoeHC6wWQrGA4YhrHuY2c"
      }
    ],
    "signing": {
      "rule": "all",
      "order": "any"
    },
    "document": {
      "id": "9114c923-0cee-4e3d-9774-4be2ebdcf629",
      "kind": "consent",
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "sent",
      "version": 1,
      "campaign": null,
      "template": {
        "id": "581c6ae4-e81f-461a-ab36-6b97e6d54b47",
        "name": "Use of information"
      },
      "expires_at": "2027-02-10T05:59:59.999Z",
      "supersedes": null
    },
    "is_resend": false,
    "signing_url": "https://example-office.engagecycle.com/p/f9ZHBiiXNQGcbnhgONCYspvqFnSjoi4cJ7hPLzagu6s"
  }
}

Only present sometimes, and shown above:

  • data.signing, data.signers: only present when the document has more than one signer.
  • data.document.client.type, data.document.client.contact_name, data.document.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).

document.viewed

{
  "id": "evt_f70c41a128894793b36239443a4337a6",
  "type": "document.viewed",
  "created_at": "2027-01-19T16:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "document": {
      "id": "9114c923-0cee-4e3d-9774-4be2ebdcf629",
      "kind": "consent",
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "viewed",
      "version": 1,
      "campaign": null,
      "template": {
        "id": "581c6ae4-e81f-461a-ab36-6b97e6d54b47",
        "name": "Use of information"
      },
      "expires_at": "2027-02-10T05:59:59.999Z",
      "supersedes": null
    },
    "viewed_at": "2027-01-19T16:00:00.000Z"
  }
}

Only present sometimes, and shown above:

  • data.document.client.type, data.document.client.contact_name, data.document.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).

document.deadline_extended

{
  "id": "evt_4ced7d61deed4e6db05b2528ba477abc",
  "type": "document.deadline_extended",
  "created_at": "2027-01-20T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "deadline": {
      "to": "2027-02-24T05:59:59.999Z",
      "from": "2027-02-10T05:59:59.999Z",
      "reason": "Waiting for the partner to answer",
      "reopened": false,
      "extended_at": "2027-01-20T15:00:00.000Z",
      "extended_by": {
        "name": "Robin Example",
        "type": "user"
      }
    },
    "document": {
      "id": "9114c923-0cee-4e3d-9774-4be2ebdcf629",
      "kind": "consent",
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "viewed",
      "version": 1,
      "campaign": null,
      "template": {
        "id": "581c6ae4-e81f-461a-ab36-6b97e6d54b47",
        "name": "Use of information"
      },
      "expires_at": "2027-02-24T05:59:59.999Z",
      "supersedes": null,
      "original_expires_at": "2027-02-10T05:59:59.999Z"
    },
    "notify_client": true,
    "signing_url": "https://example-office.engagecycle.com/p/tnWQwBpAIEwCDvrcIJn3i_BZJ9xr3RGciJWK7UkYQY0"
  }
}

Only present sometimes, and shown above:

  • data.document.client.type, data.document.client.contact_name, data.document.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).

document.signer_signed

Every signer of a document signs the same document, so chose is always false.

{
  "id": "evt_b3c4e0f0dd4847d1a7c29e954d0f8cb7",
  "type": "document.signer_signed",
  "created_at": "2027-01-21T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "signer": {
      "name": "Jordan Example",
      "chose": false,
      "title": "Owner",
      "position": 1,
      "signed_at": "2027-01-21T15:00:00.000Z",
      "signing_method": "magic_link"
    },
    "document": {
      "id": "9114c923-0cee-4e3d-9774-4be2ebdcf629",
      "kind": "consent",
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "viewed",
      "version": 1,
      "campaign": null,
      "template": {
        "id": "581c6ae4-e81f-461a-ab36-6b97e6d54b47",
        "name": "Use of information"
      },
      "expires_at": "2027-02-24T05:59:59.999Z",
      "supersedes": null,
      "original_expires_at": "2027-02-10T05:59:59.999Z"
    },
    "remaining": [
      {
        "name": "Taylor Example",
        "position": 2
      }
    ]
  }
}

Only present sometimes, and shown above:

  • data.signer.title: only present when the client is a business (the title this signer confirmed).
  • data.document.client.type, data.document.client.contact_name, data.document.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).
  • data.document.original_expires_at: only present when the deadline was extended (it is the respond-by date the document printed).

document.signer_declined

For an agreement with any one signer, as proposal.signer_declined. For a consent, every signer's decline sends it, except the one that ends the consent declined (then document.declined).

{
  "id": "evt_b20cd072c193438a8bae62602e6932df",
  "type": "document.signer_declined",
  "created_at": "2027-01-22T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "signer": {
      "name": "Taylor Example",
      "position": 2
    },
    "decline": {
      "note": "Not for me, thanks.",
      "reason": "no_thanks",
      "reason_label": "No thanks"
    },
    "document": {
      "id": "9114c923-0cee-4e3d-9774-4be2ebdcf629",
      "kind": "consent",
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "viewed",
      "version": 1,
      "campaign": null,
      "template": {
        "id": "581c6ae4-e81f-461a-ab36-6b97e6d54b47",
        "name": "Use of information"
      },
      "expires_at": "2027-02-24T05:59:59.999Z",
      "supersedes": null,
      "original_expires_at": "2027-02-10T05:59:59.999Z"
    }
  }
}

Only present sometimes, and shown above:

  • data.document.client.type, data.document.client.contact_name, data.document.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).
  • data.document.original_expires_at: only present when the deadline was extended (it is the respond-by date the document printed).

document.signed

Sent once the PDF is ready. signature.consents lists the business's consent documents the signer agreed to on an agreement. With more than one signer it adds signers (and, for an agreement any one signer may sign, could_also_sign). A consent with several signers ends signed if anyone signed, once everyone has answered or at its deadline, and then lists who declined and who never answered.

{
  "id": "evt_02d24796e0cf4c1bacdac3655e5c83a0",
  "type": "document.signed",
  "created_at": "2027-01-22T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "pdf": {
      "url": "https://app.engagecycle.com/api/v1/documents/9114c923-0cee-4e3d-9774-4be2ebdcf629/pdf",
      "sha256": "3afecf19a796e229365c6848780c40b1a8d9ea13ee7383a7ab28744d2083c9aa",
      "filename": "Consent - Example Co - 2027.pdf"
    },
    "signers": [
      {
        "name": "Jordan Example",
        "chose": false,
        "title": "Owner",
        "position": 1,
        "signed_at": "2027-01-21T15:00:00.000Z",
        "signing_method": "magic_link",
        "signing_url": null
      }
    ],
    "document": {
      "id": "9114c923-0cee-4e3d-9774-4be2ebdcf629",
      "kind": "consent",
      "client": {
        "utm": null,
        "type": "business",
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1001",
        "contact_name": "Jordan Example",
        "display_name": "Example Co",
        "contact_title": "Owner",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "signed",
      "version": 1,
      "campaign": null,
      "template": {
        "id": "581c6ae4-e81f-461a-ab36-6b97e6d54b47",
        "name": "Use of information"
      },
      "expires_at": "2027-02-24T05:59:59.999Z",
      "supersedes": null,
      "original_expires_at": "2027-02-10T05:59:59.999Z"
    },
    "signature": {
      "consents": [],
      "signed_at": "2027-01-22T15:00:00.000Z",
      "typed_name": "Jordan Example",
      "attested_by": null,
      "on_behalf_of": "Example Co",
      "signer_title": "Owner",
      "via_proposal": null,
      "signing_method": "magic_link"
    },
    "no_response": [],
    "agreed_by_all": false,
    "declined_signers": [
      {
        "name": "Taylor Example",
        "position": 2
      }
    ]
  }
}

Only present sometimes, and shown above:

  • data.declined_signers, data.no_response, data.agreed_by_all: only present when the document is a consent (with one signer they are [], [], and true).
  • data.signers: only present when the document had more than one signer (each one who signed).
  • data.signature.signer_title, data.signature.on_behalf_of, data.signers[].title: only present when the client is a business (the signer's confirmed title, and the business they signed for).
  • data.document.client.type, data.document.client.contact_name, data.document.client.contact_title: only present when the client is a business (an individual's client block has none of these keys).
  • data.document.original_expires_at: only present when the deadline was extended (it is the respond-by date the document printed).

document.declined

A consent's decline is always "reason": "no_thanks", with the client's optional note. Declining a consent never ends anything else the client signed.

{
  "id": "evt_8e6573f9b9154cfe9b4414068e4a8da9",
  "type": "document.declined",
  "created_at": "2027-01-20T16:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "decline": {
      "note": "Not right now.",
      "reason": "no_thanks",
      "reason_label": "No thanks"
    },
    "document": {
      "id": "cdde7e7d-d39a-4923-913f-cddcf66454cd",
      "kind": "consent",
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1002",
        "display_name": "Riley Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "declined",
      "version": 1,
      "campaign": null,
      "template": {
        "id": "581c6ae4-e81f-461a-ab36-6b97e6d54b47",
        "name": "Use of information"
      },
      "expires_at": "2027-02-10T05:59:59.999Z",
      "supersedes": null
    }
  }
}

document.expired

{
  "id": "evt_af2dbeba314a4990b18cc611c927f98c",
  "type": "document.expired",
  "created_at": "2027-02-14T06:59:59.999Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "document": {
      "id": "701260e9-01c6-4de9-8379-522bca6dcb6f",
      "kind": "agreement",
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1005",
        "display_name": "Harper Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "expired",
      "version": 1,
      "campaign": null,
      "template": {
        "id": "b658e541-4668-4d38-9225-ee77dcb3b68d",
        "name": "Engagement letter"
      },
      "packet_id": "08b957bf-bd78-41a5-a40b-766edfd2b7e5",
      "expires_at": "2027-02-14T05:59:59.999Z",
      "supersedes": null
    },
    "expired_at": "2027-02-14T06:59:59.999Z"
  }
}

Only present sometimes, and shown above:

  • data.document.packet_id: only present when the document is a step of a walkthrough (the walkthrough's id, as in packet.sent).

document.withdrawn

{
  "id": "evt_c99af1d24e034d2ab06a632c7ee2d2fe",
  "type": "document.withdrawn",
  "created_at": "2027-01-20T17:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "document": {
      "id": "e02dbb97-c4ac-4978-a0c9-c99388864850",
      "kind": "agreement",
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1003",
        "display_name": "Avery Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "revoked",
      "version": 1,
      "campaign": null,
      "template": {
        "id": "b658e541-4668-4d38-9225-ee77dcb3b68d",
        "name": "Engagement letter"
      },
      "expires_at": "2027-02-10T05:59:59.999Z",
      "supersedes": null
    },
    "withdrawn_at": "2027-01-20T17:00:00.000Z",
    "notify_client": false
  }
}

document.superseded

{
  "id": "evt_582935f1c78046d4aa2a75519b466049",
  "type": "document.superseded",
  "created_at": "2027-01-20T18:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "document": {
      "id": "d229d6e3-9e7f-413c-923c-2ca0dc73ed83",
      "kind": "agreement",
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1004",
        "display_name": "Quinn Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "superseded",
      "version": 1,
      "campaign": null,
      "template": {
        "id": "b658e541-4668-4d38-9225-ee77dcb3b68d",
        "name": "Engagement letter"
      },
      "expires_at": "2027-02-10T05:59:59.999Z",
      "supersedes": null
    },
    "superseded_at": "2027-01-20T18:00:00.000Z",
    "superseded_by": {
      "id": "1ade1534-7fd4-4269-9876-fe64f8209eb8",
      "version": 2
    },
    "same_recipient": true
  }
}

Walkthrough events

A walkthrough takes a client through several documents (a proposal, an agreement, a consent) and pages of the business's own words from one link. Each document in it is still its own proposal or document, with its own events carrying packet_id, except that none of them writes proposal.sent or document.sent: packet.sent stands for them. Walkthrough events go only to endpoints that tick them.

packet.sent

The walkthrough went out (or a signer's link was sent again, with is_resend: true). Email the client packet_url. steps lists the documents in order; a step that hasn't opened yet has document_id: null and status: "waiting". With more than one signer it adds signers, each with their own packet_url.

{
  "id": "evt_6dd265d7ce024dbf8a7db79e9e1f8d05",
  "type": "packet.sent",
  "created_at": "2027-01-23T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "packet": {
      "id": "b76e19a6-5cb2-4e16-9819-9a407910c19f",
      "name": "New client onboarding",
      "steps": [
        {
          "kind": "proposal",
          "title": "Renewal letter",
          "status": "sent",
          "position": 1,
          "required": true,
          "template": {
            "id": "59737070-51ca-4f42-a1e9-910035887ba9",
            "name": "Renewal letter"
          },
          "document_id": "c011617f-31c7-42bf-9efd-886ba27d3218"
        },
        {
          "kind": "agreement",
          "title": "Engagement letter",
          "status": "waiting",
          "position": 2,
          "required": true,
          "template": {
            "id": "b658e541-4668-4d38-9225-ee77dcb3b68d",
            "name": "Engagement letter"
          },
          "document_id": null
        }
      ],
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1006",
        "display_name": "Blake Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "open",
      "campaign": null,
      "expires_at": "2027-02-14T05:59:59.999Z",
      "definition_id": "4defa5db-cb58-4e75-bb14-54f100357650",
      "snapshot_sha256": "165f32f3a4b545b70d649b8fd3f0034542ba792d2128bdbdbd7c50e57127fab2"
    },
    "is_resend": false,
    "packet_url": "https://example-office.engagecycle.com/w/WSdFc349OeOXLm32U2KFP8LZ21QvO5JmKK4Et2Qu5aU"
  }
}

packet.completed

Every required step is signed: every step was answered, or the walkthrough reached its deadline with every required step signed. Sent once every signed step's PDF is ready. Each step's outcome is signed, declined, expired, or not_reached.

{
  "id": "evt_01202429e62e4ec3954ef007f28cb8f4",
  "type": "packet.completed",
  "created_at": "2027-01-24T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "steps": [
      {
        "pdf": {
          "url": "https://app.engagecycle.com/api/v1/proposals/c011617f-31c7-42bf-9efd-886ba27d3218/pdf",
          "sha256": "347a203122a849167cd0456f34af23585db7f911512230d91eab846b872ce0e7",
          "filename": "Proposal - Blake Sample - 2027.pdf"
        },
        "kind": "proposal",
        "title": "Renewal letter",
        "outcome": "signed",
        "position": 1,
        "required": true,
        "signed_at": "2027-01-24T15:00:00.000Z",
        "document_id": "c011617f-31c7-42bf-9efd-886ba27d3218"
      },
      {
        "pdf": {
          "url": "https://app.engagecycle.com/api/v1/documents/6eedd743-2a06-4250-a68c-b440a5ab1a83/pdf",
          "sha256": "c838bc73cf0ed195322a3bb6aba5267e1804040fa6c2b9296d6f4f9231d66977",
          "filename": "Agreement - Blake Sample - 2027.pdf"
        },
        "kind": "agreement",
        "title": "Engagement letter",
        "outcome": "signed",
        "position": 2,
        "required": true,
        "signed_at": "2027-01-24T15:00:00.000Z",
        "document_id": "6eedd743-2a06-4250-a68c-b440a5ab1a83"
      }
    ],
    "packet": {
      "id": "b76e19a6-5cb2-4e16-9819-9a407910c19f",
      "name": "New client onboarding",
      "client": {
        "utm": null,
        "email": "[email protected]",
        "flags": {
          "plan": "standard",
          "extra_locations": 1
        },
        "is_new": false,
        "source": "import",
        "external_id": "crm-1006",
        "display_name": "Blake Sample",
        "referral_partner": null
      },
      "origin": {
        "id": null,
        "type": "adhoc"
      },
      "status": "completed",
      "campaign": null,
      "expires_at": "2027-02-14T05:59:59.999Z",
      "definition_id": "4defa5db-cb58-4e75-bb14-54f100357650",
      "snapshot_sha256": "165f32f3a4b545b70d649b8fd3f0034542ba792d2128bdbdbd7c50e57127fab2"
    },
    "completed_at": "2027-01-24T15:00:00.000Z"
  }
}

A walkthrough that's stopped, expires, or is withdrawn shows in its documents' own events (proposal.declined, document.expired, proposal.withdrawn, and so on, each with packet_id).

Offer and lead events

A prospect on an offer becomes a lead when they enter their email, and one of your clients when they sign (or when you add them). The lead block is { id, offer, email, name, marketing_consent, referral_partner, utm }.

lead.created

A prospect entered their email on an offer. They haven't confirmed it yet.

{
  "id": "evt_5da45f4751e14709bc5a03129348016c",
  "type": "lead.created",
  "created_at": "2027-01-25T16:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "lead": {
      "id": "91526820-45f0-4063-bbc0-c16b5a12b112",
      "utm": {
        "source": "qr",
        "campaign": "spring-flyer"
      },
      "name": "Drew Sample",
      "email": "[email protected]",
      "offer": {
        "id": "5862e0a2-8f35-41c2-934e-86a0a88edddc",
        "slug": "new-clients"
      },
      "referral_partner": {
        "code": "LAKESIDE",
        "name": "Lakeside Partners"
      },
      "marketing_consent": true
    }
  }
}

lead.verified

The prospect confirmed their email, and has a proposal (data.proposal, created already sent, so there's no proposal.sent for it). answers are their answers, keyed by question id. outcome is new_client, existing_client_open_proposal (they kept the open proposal they already had), or existing_client_flagged (someone you already have took a new proposal from the offer; their details are left for staff to review). A request staff sent after holding it for review adds "reviewed": true, and follows its proposal.sent.

{
  "id": "evt_bd6bd13052304d12bb7ab90a85ccec97",
  "type": "lead.verified",
  "created_at": "2027-01-25T16:06:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "lead": {
      "id": "91526820-45f0-4063-bbc0-c16b5a12b112",
      "utm": {
        "source": "qr",
        "campaign": "spring-flyer"
      },
      "name": "Drew Sample",
      "email": "[email protected]",
      "offer": {
        "id": "5862e0a2-8f35-41c2-934e-86a0a88edddc",
        "slug": "new-clients"
      },
      "referral_partner": {
        "code": "LAKESIDE",
        "name": "Lakeside Partners"
      },
      "marketing_consent": true
    },
    "answers": {
      "billing": "annual",
      "locations": 2
    },
    "outcome": "new_client",
    "proposal": {
      "id": "b49d2048-1adc-4160-a5d4-ac3ed3ef19d0",
      "booked": false,
      "client": {
        "utm": {
          "source": "qr",
          "campaign": "spring-flyer"
        },
        "email": "[email protected]",
        "flags": {},
        "is_new": true,
        "source": "offer",
        "external_id": null,
        "display_name": "Drew Sample",
        "referral_partner": {
          "code": "LAKESIDE",
          "name": "Lakeside Partners"
        }
      },
      "origin": {
        "id": "5862e0a2-8f35-41c2-934e-86a0a88edddc",
        "slug": "new-clients",
        "type": "offer"
      },
      "status": "sent",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-16T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "standard"
      ]
    }
  }
}

lead.awaiting_review

The prospect confirmed their email on an offer that holds requests for review. Nothing has been sent to them yet except a thank-you. existing_client is true when their email belongs to one of your clients, and expires_at is when the request closes if nobody acts.

{
  "id": "evt_19b1a9e6a03c439d99405eed6b4651f8",
  "type": "lead.awaiting_review",
  "created_at": "2027-01-26T17:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "lead": {
      "id": "51642322-4b8e-43bb-b838-4b4cb6f56937",
      "utm": {
        "source": "qr",
        "campaign": "spring-flyer"
      },
      "name": "Rowan Sample",
      "email": "[email protected]",
      "offer": {
        "id": "aa6961fa-53b2-4d1a-a522-d9b0e242b216",
        "slug": "request-a-quote"
      },
      "referral_partner": null,
      "marketing_consent": false
    },
    "answers": {
      "billing": "annual",
      "locations": 2
    },
    "expires_at": "2027-02-25T17:00:00.000Z",
    "existing_client": false
  }
}

lead.declined

A held request closed without a proposal: reason is declined (staff declined it) or expired (nobody acted in time). email_sent says whether the prospect was emailed.

{
  "id": "evt_3e41bbeec0244089bd519dbd7b219670",
  "type": "lead.declined",
  "created_at": "2027-01-27T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "lead": {
      "id": "51642322-4b8e-43bb-b838-4b4cb6f56937",
      "utm": {
        "source": "qr",
        "campaign": "spring-flyer"
      },
      "name": "Rowan Sample",
      "email": "[email protected]",
      "offer": {
        "id": "aa6961fa-53b2-4d1a-a522-d9b0e242b216",
        "slug": "request-a-quote"
      },
      "referral_partner": null,
      "marketing_consent": false
    },
    "reason": "declined",
    "email_sent": false
  }
}

lead.abandoned

The prospect confirmed their email but hasn't signed after the offer's follow-up time. resume_url takes them back to where they left off, while it still works.

{
  "id": "evt_c02baf6cff0c48b2b8ad654075232287",
  "type": "lead.abandoned",
  "created_at": "2027-01-28T17:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "lead": {
      "id": "16695729-ef72-4c73-9cd5-0cb2493e8224",
      "utm": {
        "source": "qr",
        "campaign": "spring-flyer"
      },
      "name": "Sky Sample",
      "email": "[email protected]",
      "offer": {
        "id": "5862e0a2-8f35-41c2-934e-86a0a88edddc",
        "slug": "new-clients"
      },
      "referral_partner": {
        "code": "LAKESIDE",
        "name": "Lakeside Partners"
      },
      "marketing_consent": false
    },
    "proposal": {
      "id": "8d05c59e-1890-4f52-bcd2-381cfe3abfb8",
      "booked": false,
      "client": {
        "utm": {
          "source": "qr",
          "campaign": "spring-flyer"
        },
        "email": "[email protected]",
        "flags": {},
        "is_new": true,
        "source": "offer",
        "external_id": null,
        "display_name": "Sky Sample",
        "referral_partner": {
          "code": "LAKESIDE",
          "name": "Lakeside Partners"
        }
      },
      "origin": {
        "id": "5862e0a2-8f35-41c2-934e-86a0a88edddc",
        "slug": "new-clients",
        "type": "offer"
      },
      "status": "sent",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-17T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "standard"
      ]
    },
    "resume_url": "https://example-office.engagecycle.com/r/OheP1f0zTJj8pQ6quu9J1R-Yt9xLErokWf5FAjRKmGU"
  }
}

client.created

An offer's prospect became one of your clients, once: became_client_by is signed, staff (added as a client from the lead), or imported (an import, Add client, or POST /api/v1/clients with their email). Use this to create the client in your other systems.

{
  "id": "evt_8f312604a807425e83a604bc3bc2b712",
  "type": "client.created",
  "created_at": "2027-01-25T18:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "client": {
      "id": "da04828c-6f6c-424a-8057-02ee5217379d",
      "email": "[email protected]",
      "source": "offer",
      "external_id": null,
      "display_name": "Drew Sample",
      "became_client_at": "2027-01-25T18:00:00.000Z"
    },
    "source": {
      "utm": {
        "source": "qr",
        "campaign": "spring-flyer"
      },
      "offer": {
        "id": "5862e0a2-8f35-41c2-934e-86a0a88edddc",
        "slug": "new-clients"
      },
      "referral_partner": {
        "code": "LAKESIDE",
        "name": "Lakeside Partners"
      }
    },
    "proposal": {
      "id": "b49d2048-1adc-4160-a5d4-ac3ed3ef19d0",
      "booked": false,
      "client": {
        "utm": {
          "source": "qr",
          "campaign": "spring-flyer"
        },
        "email": "[email protected]",
        "flags": {},
        "is_new": true,
        "source": "offer",
        "external_id": null,
        "display_name": "Drew Sample",
        "referral_partner": {
          "code": "LAKESIDE",
          "name": "Lakeside Partners"
        }
      },
      "origin": {
        "id": "5862e0a2-8f35-41c2-934e-86a0a88edddc",
        "slug": "new-clients",
        "type": "offer"
      },
      "status": "signed",
      "version": 1,
      "campaign": null,
      "expires_at": "2027-02-16T05:59:59.999Z",
      "supersedes": null,
      "visible_options": [
        "standard"
      ]
    },
    "became_client_by": "signed"
  }
}

Only present sometimes, and shown above:

  • data.proposal: only present when they became a client by signing (became_client_by is signed).

Email events

email.bounced

An email to a client hard bounced, reported by your business's own email sender (Resend, Postmark, or Brevo) or EngageCycle's. The address is now suppressed: stop emailing it from your own tools too. proposal_id is null for mail that isn't about a proposal. SMTP senders don't report bounces.

{
  "id": "evt_4c7aa8434b7648609272a9ecd733eec3",
  "type": "email.bounced",
  "created_at": "2027-01-29T15:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "email": {
      "id": "190408a9-366f-42db-ba86-fe5535be2a68",
      "to": "[email protected]",
      "kind": "proposal_link",
      "sender": "postmark",
      "proposal_id": "6ad29928-c32b-439a-874e-c600a32dec24"
    },
    "bounce": {
      "at": "2027-01-29T15:00:00.000Z",
      "detail": "Mailbox does not exist"
    },
    "suppressed": true
  }
}

email.complained

The client marked one of your emails as spam. The same shape, with complaint in place of bounce.

{
  "id": "evt_4a4c572602e4424ea913af8c1ae1157c",
  "type": "email.complained",
  "created_at": "2027-01-29T16:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "email": {
      "id": "567c63b1-e634-44a8-a35b-b1ba73c74d87",
      "to": "[email protected]",
      "kind": "proposal_link",
      "sender": "postmark",
      "proposal_id": "643ccda8-513c-4315-8e49-a33d208df121"
    },
    "complaint": {
      "at": "2027-01-29T16:00:00.000Z",
      "detail": "Reported as spam"
    },
    "suppressed": true
  }
}

Test events

webhook.test

Sent by Send test event in Integrations, to that one endpoint. Your automation can ignore it.

{
  "id": "evt_fc68aa54db664dacba7f4205a8377584",
  "type": "webhook.test",
  "created_at": "2027-01-29T17:00:00.000Z",
  "tenant": {
    "id": "2a11b7e2-6b39-41ea-b05f-001566400f1d",
    "slug": "example-office"
  },
  "data": {
    "by": "[email protected]",
    "test": true,
    "sent_at": "2027-01-29T17:00:00.000Z"
  }
}

Polling instead

GET /api/v1/events lists every event in the same envelope, oldest first; ?since=<next_cursor> returns only what's new, and type= filters (repeat it for several). It returns document.* and packet.* only when a request names them in type. Payloads are the same as delivered, except that links are null (with "redacted": true) unless the API key has proposals:links. Each item adds delivery: delivered, due, failed, or off (no endpoint took it). Event ids are the same both ways, so you can take both and dedupe on id. The API reference has the details, and the whole set of events is also in the OpenAPI document's webhooks section at /api/v1/openapi.json.