Boarding Form Webhook Payloads

Payload reference for the five form.* lifecycle events, from creation through completion or expiration.

Boarding form webhooks track a merchant's progress through onboarding in near real time — when a form is created, when the merchant first saves progress, every time they save, when they submit, and when an unfinished form lapses.

Together they replace polling the Forms API to discover state changes, and they make drop-off visible: you can see which merchants opened a form and stopped.

EventFires whenTimes per form
form.createdA boarding form is created.Once
form.startedThe merchant completes a section of the form for the first time.Once
form.updatedAny change is saved to the form.Once per save
form.completedThe form is submitted and its completion recorded.Once
form.expiredAn unfinished form passes its expiration time.Once

For creating and configuring the forms themselves, see Boarding Forms.

Lifecycle

flowchart TD
    A["Form created"] -->|form.created| B["Status: ACTIVE"]
    B --> C{"Merchant saves<br/>first section?"}
    C -->|"Yes — first save only"| D["form.started<br/>+ form.updated"]
    C -->|"Never — deadline passes"| E["form.expired<br/>Status: EXPIRED"]
    D --> F{"More saves?"}
    F -->|"Each save"| G["form.updated"]
    G --> F
    F -->|"Submitted"| H["form.completed<br/>+ form.updated<br/>Status: COMPLETED"]
    F -->|"Abandoned past deadline"| E
❗️

form.updated fires alongside the other events, not instead of them

Every save emits form.updated, and that includes the save that first records progress and the save that completes the form. So the first save delivers both form.started and form.updated, and submission delivers both form.completed and form.updated.

If you subscribe to form.updated together with the others, expect two notifications for those moments. Branch on eventType rather than assuming one notification per state change.

Payload

All five events share one envelope. data carries identifiers, status, and timestamps — plus the contact block for the person filling out the form, when one has been captured.

{
  "id": "what_test_6knw5fhtys9egrxtd6bt0txjhj",
  "webhookId": "wh_test_57vn6accfv9hrb5kqehv3k47tx",
  "eventType": "form.completed",
  "data": {
    "formId": "form_test_3r6j69gwvm81gapafn6c0dtmg2",
    "merchantId": "mid_test_wsqgngrn99gptndb1h2s70dzj",
    "status": "COMPLETED",
    "lastSectionCompleted": "banking",
    "completionDate": "2026-07-18T09:41:55Z",
    "modifiedOn": "2026-07-18T09:41:55Z",
    "personCompleting": {
      "firstName": "Jane",
      "lastName": "Roe",
      "email": "[email protected]",
      "phone": "+15125550143",
      "title": "CFO",
      "agreedOn": "2026-07-18T09:41:52Z",
      "ipAddress": "203.0.113.42"
    }
  }
}
📘

Form contents are never sent

The payload carries identifiers, status values, and timestamps only — never the business, owner, banking, or document data captured by the form, and never the form template. To read the submitted data, fetch the form or the merchant through the API once you receive the notification.

Fields by event

Fields with no value are omitted rather than sent as null, so the exact key set depends on the event.

Fieldcreatedstartedupdatedcompletedexpired
formId
merchantId
status
createdOn
expiresOn
lastSectionCompleted
completionDate
modifiedOn
personCompleting◻️◻️◻️◻️◻️

✅ always present ◻️ present only once captured

FieldDescription
formIdThe boarding form's ID. Use this to fetch the form through the API.
merchantIdThe merchant the form belongs to, prefixed mid_.
statusThe form's lifecycle status: ACTIVE, COMPLETED, or EXPIRED.
createdOnWhen the form was created, ISO 8601 UTC.
expiresOnThe form's expiration deadline, ISO 8601 UTC.
lastSectionCompletedThe most recent section the merchant finished. Use it to gauge progress.
completionDateWhen the form was submitted, ISO 8601 UTC.
modifiedOnWhen this save occurred, ISO 8601 UTC. Use it to discard stale notifications.
personCompletingThe contact filling out the form. See below.

personCompleting

Present once the person completing the form has identified themselves. Every field within it is optional, and the block is omitted entirely if no values have been captured yet — so a form.created notification usually has no personCompleting, while a form.completed normally does.

FieldDescription
firstName, lastNameThe person's name.
emailTheir email address.
phoneTheir phone number, in E.164 format.
titleTheir role at the business, such as CFO.
agreedOnWhen they accepted the platform agreement, ISO 8601 UTC. Recorded by Preczn, never supplied by the client.
ipAddressThe IP address the agreement was accepted from. Recorded by Preczn, never supplied by the client.

agreedOn and ipAddress are stamped server-side, which is what makes them usable as evidence of acceptance. See Welcome Section for how the agreement is presented.

Event semantics

form.started means first saved progress

form.started fires when the merchant completes a section of the form for the first time — not when they open the link or view the first page. A merchant who opens a form and closes it without saving anything will never produce form.started.

This makes it a meaningful engagement signal rather than a page-view counter: a form with form.created but no form.started was never genuinely begun, which is a different re-engagement problem from one abandoned halfway through.

form.expired is an hourly sweep

Expiration is detected by a job that runs hourly, so form.expired arrives within about an hour of the deadline in expiresOn rather than exactly at it. Every other form event is emitted as it happens.

The sweep only considers forms whose status is ACTIVE. Two consequences: a form completed before its deadline is not eligible and will never emit form.expired even after expiresOn passes, and a form carrying no status value at all is never swept.

📘

expiresOn is fixed when the form is created

The deadline is calculated at creation from the form's expiration window and then frozen. Editing the expiration setting on a form template afterwards does not move the deadline on forms that already exist — those keep the expiresOn they were created with.

Delivery guarantees

form.created, form.started, form.completed, and form.expired are each designed to fire exactly once per form. form.updated fires once per save and is intentionally not once-only.

As with every Preczn webhook, this is not a substitute for idempotency in your handler — a retry can redeliver any notification. Deduplicate on the top-level id, key your updates off formId, and compare modifiedOn against your stored state. See Receiving and Verifying Webhooks.

FAQ

Which events should I subscribe to?

It depends on what you are building.

For activating merchants automatically, form.completed alone is usually enough — it is the signal that the data is in and you can trigger your next step.

For re-engagement, add form.created and form.expired. A form created but never started, or one that expired, is a merchant worth emailing.

For a live progress indicator, add form.started and form.updated and read lastSectionCompleted. Be aware form.updated is the chattiest event, since it fires on every save.

Why did I get two notifications when the merchant submitted the form?

Because submission is also a save. It emits form.completed and form.updated, and both are delivered if you subscribe to both. The same applies to the first save, which emits form.started and form.updated.

This is deliberate — form.updated means "the form changed", and those moments are changes. Branch on eventType and ignore the form.updated when you have already handled the more specific event.

How many form.updated notifications should I expect?

One per save, which during active form-filling can be many — merchants save as they work through sections. There is no batching or debouncing.

If that volume is more than you need, subscribe to form.started and form.completed instead and skip form.updated entirely. Only subscribe to it if you genuinely need to react to intermediate saves.

Can I get the data the merchant entered from the webhook?

No — payloads deliberately exclude all form contents. Use the formId or merchantId to fetch the submitted data through the API once you receive the notification. This keeps sensitive onboarding data out of webhook traffic and delivery history, where it would otherwise sit in the clear.

Does form.completed mean the merchant can start processing?

No. It means the form was submitted, not that the processor has approved the merchant. Approval is a separate, asynchronous step by the downstream processor.

Watch connections[].status on merchant.updated for the connection reaching a ready state — see Merchant Webhook Payloads and Merchant Lifecycle.

Do I get an event when a merchant opens the form link?

No. The earliest engagement signal is form.started, which requires the merchant to save a section. There is no page-view or link-open event.

What happens if a merchant completes a form after it expired?

They cannot — an expired form is no longer fillable, and the merchant is directed to the form's expiration URL. Issue a new form to let them continue.

Note that the reverse race is handled: because the sweep only looks at forms still ACTIVE, a form completed just before its deadline will not subsequently be marked expired.

Do forms scoped to multiple connections emit one set of events or several?

One set. The events describe the form, not the connections attached to it, so a form covering several processor connections still emits a single form.created, form.completed, and so on. See Multi-Connection Boarding Forms.


Did this page help you?