Topolo Docs Topolo Platform
Open workspace

Update a form’s delivery settings

Save a reviewed channel configuration with version protection and explicit recipient/destination checks.

Outcome

Save a reviewed channel configuration with version protection and explicit recipient/destination checks.

PUT /api/forms/{formId}/delivery

Before you start

Use the original authorized organization and a discovered canonical Forms workspace, bound separately as workspace resource context. Resolve formId through authorized Forms reads. Input workspace ids alone grant no access.

GET requires application workspace:read and a bearer workspace read grant. PUT requires application workspace:write plus bearer workspace admin because it controls routing of submitted personal data. API keys retain the exact Auth-verified resource binding and actual application permission; no unimplemented local API-key admin check is asserted.

Public guides contain fictional source-derived UNRUN responses. Real confirmation copy, recipient directories, destinations, project mappings and operational output remain within the authorized audience. No actual private read, configuration, send, CRM/Consent/Nexus/model/key operation was executed.

Request and behavior

Action: app_topolo_forms.delivery_settings.update. Validate the exact published input and bind workspace separately. Plan and confirm the actual reviewed update before execution.

PUT reads at most32000 body bytes, loads current settings and merges supplied sections. Omitted sections/most omitted keys retain stored values. When a confirmation object is supplied, subjectMode and bodyMode must both explicitly be default or custom even if changing only enabled. Default mode clears stored custom text; Newly supplied custom subject/body text must be nonblank and within200/5000 characters. Omitted custom text uses the existing text when its current mode is custom, otherwise an empty-string fallback. Switching from default to custom while omitting subject/body can therefore save empty copy; omission is not a nonblank guarantee.

If expectedVersion is omitted, the route uses the version it just read. To protect an earlier reviewed edit, explicitly supply the exact version returned by delivery_settings.get. The store rereads current version, protects full merged settings, uses UPDATE WHEREid/version or INSERT ORIGNORE, and requires exactly1writtenrow. Even an otherwise unchanged accepted save increments version; it is not an idempotent no-op.

Field mappings are checked against the current form when supplied. Confirmation/CRM/Consent identity fields allow email or hidden; CRM full name also permits name, phone has its narrower types, and UTM allows hidden/short_text/select/radio. Existing omitted mappings are retained rather than all revalidated. Enabled confirmation/CRM require an email field. Enabled Consent requires projectKey,identityFieldId and1..10mappings using consent/checkbox fields with distinct field ids and nonemptypurposeids; this does not validate an actual Consent project/purpose or establish lawful consent.

Notification recipient ids are trimmed/deduplicated, maximum20, with nonempty allowed channels in_app/email. Enabled notifications require at least one recipient. If any new recipient is added relative to stored settings, the route fetches the directory: unavailable→503 recipient_directory_unavailable; unknown ids→422 notification_recipient_not_member with recipientUserIds. Removing recipients alone does not recheck all retained members.

Outbound mappings require distinct destination ids starting dst_, at most20, and distinct current field ids. Enabling outbound requires nonempty destinations/fields. Whenever outbound remains enabled, route resolves enabled destinations; unavailable→503 destination_directory_unavailable; unavailable/disabledselecteddestination→422 destination_not_available. Successful settings save does not dispatch these destinations or verify their external provider.

Accepted full settings are protected/persisted with the actor id, updated timestamp and version+1. HTTP200 is {delivery:<stored model>,capabilities:<presence flags>}. It has no required success:true/id wrapper and does not include the GET-only destinations/nexusUrl/recipients.

A conflict returns409 {error:version_conflict,currentVersion:<route initial read>}; concurrent changes after that initial read can make this hint stale. Always reread before a deliberately revised/approved retry; never blindly replace expectedVersion with the hint. Protection/key work can precede the row conflict, so no total side-effect-free claim is made.

Saving settings does not immediately enqueue/backfill existing responses, send a confirmation, create CRM records, set Consent or run outbound actions. New submissions snapshot enabled delivery configuration; existing snapshots are not rewritten by this PUT. A later explicitly approved retry may create a missing channel row using current configuration; it is a separate potentially external mutation.

Stored settings and readiness

The selected source first reads the form under the original organization/workspace. Missing or out-of-scope form returns404 {error:not_found}. Stored settings JSON is protected under the environment-aware policy and unprotected for an authorized reader. Failure is not permission to downgrade protection or substitute another tenant.

Five channel sections are confirmation,notification,crm,consent,outbound. Missing stored row returns complete defaults, version0,configuredfalse and null update metadata without inserting a row. A stored row returns configuredtrue, numeric version and update metadata. configured means a row exists, not that any channel is enabled or working.

Capability flags are configuration-presence checks, not live readiness. Confirmation/notification need Notify URL plus service credential; CRM/outbound use Nexus URL plus service credential; Consent checks its URL. Flags do not verify recipient permission, service reachability, external provider setup, project validity or delivery outcomes.

Real settings and directory responses can contain user names/email, copy, destination ids and consent project/mapping data. They are authenticated application data; this public guide makes none of that content public.

Worked examples

Save a reviewed confirmation mapping

Complete source-derived synthetic UNRUN HTTP200. Current version0/default settings and actual current email field named email; no concurrent editor. Actor is fictional usr_docs_fixture, store timestamp fixed for illustration. No notification or outbound changes require a directory check. Saving can succeed while presence flags remainfalse; no send occurs.

Input:

{
  "formId": "forms_docs_fixture",
  "expectedVersion": 0,
  "confirmation": {
    "enabled": true,
    "emailFieldId": "email",
    "subjectMode": "default",
    "bodyMode": "default"
  }
}

Response:

{
  "delivery": {
    "formId": "forms_docs_fixture",
    "settings": {
      "confirmation": {
        "enabled": true,
        "emailFieldId": "email",
        "subjectMode": "default",
        "bodyMode": "default",
        "subject": "",
        "body": "",
        "replyTo": null
      },
      "notification": {
        "enabled": false,
        "recipientUserIds": [],
        "channels": [
          "in_app",
          "email"
        ]
      },
      "crm": {
        "enabled": false,
        "leadSource": "Topolo Forms",
        "tags": null,
        "emailFieldId": null,
        "fullNameFieldId": null,
        "firstNameFieldId": null,
        "lastNameFieldId": null,
        "companyFieldId": null,
        "phoneFieldId": null,
        "jobTitleFieldId": null,
        "utmSourceFieldId": null,
        "utmMediumFieldId": null,
        "utmCampaignFieldId": null,
        "utmTermFieldId": null,
        "utmContentFieldId": null
      },
      "consent": {
        "enabled": false,
        "projectKey": null,
        "identityFieldId": null,
        "region": null,
        "mappings": []
      },
      "outbound": {
        "enabled": false,
        "destinationIds": [],
        "fieldIds": []
      }
    },
    "version": 1,
    "configured": true,
    "updatedAt": "2026-10-21T00:00:00.000Z",
    "updatedByUserId": "usr_docs_fixture"
  },
  "capabilities": {
    "confirmation": {
      "available": false,
      "reason": "notify_not_configured"
    },
    "notification": {
      "available": false,
      "reason": "notify_not_configured"
    },
    "outbound": {
      "available": false,
      "reason": "nexus_not_configured"
    },
    "crm": {
      "available": false,
      "reason": "crm_not_configured"
    },
    "consent": {
      "available": false,
      "reason": "consent_not_configured"
    }
  }
}

Compare only with the actual authorized target and documented state. Fictional examples authorize no live configuration, delivery or private-directory operation.

An earlier reviewed version is stale

Complete source-derived synthetic UNRUN HTTP409. Route initial read and store read both seeversion1. The hint may differ under further concurrency; no successful setting-row write in this example.

Input:

{
  "formId": "forms_docs_fixture",
  "expectedVersion": 0,
  "notification": {
    "enabled": false
  }
}

Response:

{
  "error": "version_conflict",
  "currentVersion": 1
}

Compare only with the actual authorized target and documented state. Fictional examples authorize no live configuration, delivery or private-directory operation.

New notification recipient is not in the returned directory

Complete source-derived synthetic UNRUN HTTP422. Successful fictional organization directory does not contain the newly requested id; refused before settings save or send.

Input:

{
  "formId": "forms_docs_fixture",
  "expectedVersion": 0,
  "notification": {
    "enabled": true,
    "recipientUserIds": [
      "usr_absent_docs_fixture"
    ]
  }
}

Response:

{
  "error": "notification_recipient_not_member",
  "recipientUserIds": [
    "usr_absent_docs_fixture"
  ]
}

Compare only with the actual authorized target and documented state. Fictional examples authorize no live configuration, delivery or private-directory operation.

Verify and recover

  1. Keep actual formId,original organization/workspace and the exact settings version. For updates, compare full returned settings and updatedByUserId with the approved change, then authorized GET version/content read-back; do not replay a lost successful save.

  2. Do not interpret configured,enabled or capabilitypresence as actual delivery/readiness. Directory null/unavailable, staleness, revoked recipients and external outcomes require their own supported verification and authority.

  3. Preserve sanitized request/status evidence on auth/database/protection/integration failures. No test send, export, retry, provider call or key change is authorized as a documentation check.

For an intentional rollback, reread the latest version, review the retained prior settings and valid current mappings, then separately plan/approve a new update using that version. No automatic undo of already-snapshotted or external delivery effects.

Errors

404 / not_found: Reconcile the discovered form id and original authorized workspace.

403 / forbidden: Keep the denial and original identity; seek legitimate grant from its owner.

Shared auth/protection errors retain their actual service carrier; local schema rejection is not a captured handler response. The update constraints and concrete conflicts/directories errors are described above.

Change log

  • 2026-10-21: Added exact full settings model, configuration/readiness distinction, directory and routing boundaries, versioned update behavior, complete synthetic success/refusal responses and scoped recovery. Examples remain unrun.