# Get action run log Source: https://docs.getmembrane.com/api-reference/actionrunlogs/get-action-run-log /api-reference/membrane-api.json get /action-run-logs/{id} # Get action run log details Source: https://docs.getmembrane.com/api-reference/actionrunlogs/get-action-run-log-details /api-reference/membrane-api.json get /action-run-logs/{id}/details # List action run logs Source: https://docs.getmembrane.com/api-reference/actionrunlogs/list-action-run-logs /api-reference/membrane-api.json get /action-run-logs # Archive action Source: https://docs.getmembrane.com/api-reference/actions/archive-action /api-reference/membrane-api.json delete /actions/{selector} # Clone action Source: https://docs.getmembrane.com/api-reference/actions/clone-action /api-reference/membrane-api.json post /actions/{selector}/clone # Create a new action Source: https://docs.getmembrane.com/api-reference/actions/create-a-new-action /api-reference/membrane-api.json post /actions Creates an integration-level action (workspace-wide) or a connection-level action instance when `connectionId` is provided. Supports intent-based creation: include an `intent` field to launch an action-building agent. # Export action (flat interface) Source: https://docs.getmembrane.com/api-reference/actions/export-action-flat-interface /api-reference/membrane-api.json get /actions/{selector}/export # Get action by selector Source: https://docs.getmembrane.com/api-reference/actions/get-action-by-selector /api-reference/membrane-api.json get /actions/{selector} # Get dependencies of action (flat interface) Source: https://docs.getmembrane.com/api-reference/actions/get-dependencies-of-action-flat-interface /api-reference/membrane-api.json get /actions/{selector}/dependencies # List actions Source: https://docs.getmembrane.com/api-reference/actions/list-actions /api-reference/membrane-api.json get /actions # Refresh action Source: https://docs.getmembrane.com/api-reference/actions/refresh-action /api-reference/membrane-api.json post /actions/{selector}/refresh # Replace action completely Source: https://docs.getmembrane.com/api-reference/actions/replace-action-completely /api-reference/membrane-api.json put /actions/{selector} # Reset action Source: https://docs.getmembrane.com/api-reference/actions/reset-action /api-reference/membrane-api.json post /actions/{selector}/reset Refreshes all the dependencies, then resets the element to the default state (from parent) # Restore action (flat interface) Source: https://docs.getmembrane.com/api-reference/actions/restore-action-flat-interface /api-reference/membrane-api.json post /actions/{selector}/restore # Update action partially Source: https://docs.getmembrane.com/api-reference/actions/update-action-partially /api-reference/membrane-api.json patch /actions/{selector} # Archive app data schema by selector Source: https://docs.getmembrane.com/api-reference/appdataschemas/archive-app-data-schema-by-selector /api-reference/membrane-api.json delete /app-data-schemas/{idOrKey} # Create app data schema Source: https://docs.getmembrane.com/api-reference/appdataschemas/create-app-data-schema /api-reference/membrane-api.json post /app-data-schemas # Export app data schema by selector Source: https://docs.getmembrane.com/api-reference/appdataschemas/export-app-data-schema-by-selector /api-reference/membrane-api.json get /app-data-schemas/{idOrKey}/export # Get app data schema by selector Source: https://docs.getmembrane.com/api-reference/appdataschemas/get-app-data-schema-by-selector /api-reference/membrane-api.json get /app-data-schemas/{idOrKey} # Get dependencies of a app data schema Source: https://docs.getmembrane.com/api-reference/appdataschemas/get-dependencies-of-a-app-data-schema /api-reference/membrane-api.json get /app-data-schemas/{elementSelector}/dependencies # List app data schemas Source: https://docs.getmembrane.com/api-reference/appdataschemas/list-app-data-schemas /api-reference/membrane-api.json get /app-data-schemas # Patch app data schema by selector Source: https://docs.getmembrane.com/api-reference/appdataschemas/patch-app-data-schema-by-selector /api-reference/membrane-api.json patch /app-data-schemas/{idOrKey} # Refresh app data schema by selector Source: https://docs.getmembrane.com/api-reference/appdataschemas/refresh-app-data-schema-by-selector /api-reference/membrane-api.json post /app-data-schemas/{idOrKey}/refresh # Restore (unarchive) app data schema Source: https://docs.getmembrane.com/api-reference/appdataschemas/restore-unarchive-app-data-schema /api-reference/membrane-api.json post /app-data-schemas/{elementSelector}/restore # Update app data schema by selector Source: https://docs.getmembrane.com/api-reference/appdataschemas/update-app-data-schema-by-selector /api-reference/membrane-api.json put /app-data-schemas/{idOrKey} # Get internal event log record Source: https://docs.getmembrane.com/api-reference/appeventlogrecords/get-internal-event-log-record /api-reference/membrane-api.json get /app-events/{id} # List internal event log records Source: https://docs.getmembrane.com/api-reference/appeventlogrecords/list-internal-event-log-records /api-reference/membrane-api.json get /app-events # Archive internal event subscription by id Source: https://docs.getmembrane.com/api-reference/appeventsubscriptions/archive-internal-event-subscription-by-id /api-reference/membrane-api.json delete /app-event-subscriptions/{id} # Get dependencies of a app event subscription Source: https://docs.getmembrane.com/api-reference/appeventsubscriptions/get-dependencies-of-a-app-event-subscription /api-reference/membrane-api.json get /app-event-subscriptions/{elementSelector}/dependencies # Get internal event subscription by id Source: https://docs.getmembrane.com/api-reference/appeventsubscriptions/get-internal-event-subscription-by-id /api-reference/membrane-api.json get /app-event-subscriptions/{id} # Get internal event subscription schema by id Source: https://docs.getmembrane.com/api-reference/appeventsubscriptions/get-internal-event-subscription-schema-by-id /api-reference/membrane-api.json get /app-event-subscriptions/{id}/schema # List internal event subscriptions Source: https://docs.getmembrane.com/api-reference/appeventsubscriptions/list-internal-event-subscriptions /api-reference/membrane-api.json get /app-event-subscriptions # Refresh app event subscription Source: https://docs.getmembrane.com/api-reference/appeventsubscriptions/refresh-app-event-subscription /api-reference/membrane-api.json post /app-event-subscriptions/{elementSelector}/refresh # Restore (unarchive) app event subscription Source: https://docs.getmembrane.com/api-reference/appeventsubscriptions/restore-unarchive-app-event-subscription /api-reference/membrane-api.json post /app-event-subscriptions/{elementSelector}/restore # Subscribe to internal event subscription by id Source: https://docs.getmembrane.com/api-reference/appeventsubscriptions/subscribe-to-internal-event-subscription-by-id /api-reference/membrane-api.json post /app-event-subscriptions/{id}/subscribe # Update internal event subscription by id Source: https://docs.getmembrane.com/api-reference/appeventsubscriptions/update-internal-event-subscription-by-id /api-reference/membrane-api.json put /app-event-subscriptions/{id} # Archive internal event type Source: https://docs.getmembrane.com/api-reference/appeventtypes/archive-internal-event-type /api-reference/membrane-api.json delete /app-event-types/{id} # Create internal event type Source: https://docs.getmembrane.com/api-reference/appeventtypes/create-internal-event-type /api-reference/membrane-api.json post /app-event-types # Export Internal Event Type to JSON Source: https://docs.getmembrane.com/api-reference/appeventtypes/export-internal-event-type-to-json /api-reference/membrane-api.json get /app-event-types/{id}/export # Get dependencies of a app event type Source: https://docs.getmembrane.com/api-reference/appeventtypes/get-dependencies-of-a-app-event-type /api-reference/membrane-api.json get /app-event-types/{elementSelector}/dependencies # Get internal event type Source: https://docs.getmembrane.com/api-reference/appeventtypes/get-internal-event-type /api-reference/membrane-api.json get /app-event-types/{id} # Get internal event type schema Source: https://docs.getmembrane.com/api-reference/appeventtypes/get-internal-event-type-schema /api-reference/membrane-api.json get /app-event-types/{id}/schema # List internal event types Source: https://docs.getmembrane.com/api-reference/appeventtypes/list-internal-event-types /api-reference/membrane-api.json get /app-event-types # Patch internal event type Source: https://docs.getmembrane.com/api-reference/appeventtypes/patch-internal-event-type /api-reference/membrane-api.json patch /app-event-types/{id} # Refresh app event type Source: https://docs.getmembrane.com/api-reference/appeventtypes/refresh-app-event-type /api-reference/membrane-api.json post /app-event-types/{elementSelector}/refresh # Restore (unarchive) app event type Source: https://docs.getmembrane.com/api-reference/appeventtypes/restore-unarchive-app-event-type /api-reference/membrane-api.json post /app-event-types/{elementSelector}/restore # Update internal event type Source: https://docs.getmembrane.com/api-reference/appeventtypes/update-internal-event-type /api-reference/membrane-api.json put /app-event-types/{id} # List public apps Source: https://docs.getmembrane.com/api-reference/appsuniverse/list-public-apps /api-reference/membrane-api.json get /universe/apps # Archive a connection Source: https://docs.getmembrane.com/api-reference/connections/archive-a-connection /api-reference/membrane-api.json delete /connections/{id} # Create a new connection Source: https://docs.getmembrane.com/api-reference/connections/create-a-new-connection /api-reference/membrane-api.json post /connections # Export connection configuration Source: https://docs.getmembrane.com/api-reference/connections/export-connection-configuration /api-reference/membrane-api.json get /connections/{id}/export # Get connection by ID Source: https://docs.getmembrane.com/api-reference/connections/get-connection-by-id /api-reference/membrane-api.json get /connections/{id} # Get connection logs Source: https://docs.getmembrane.com/api-reference/connections/get-connection-logs /api-reference/membrane-api.json get /connections/{elementSelector}/logs # Get dependencies of a connection Source: https://docs.getmembrane.com/api-reference/connections/get-dependencies-of-a-connection /api-reference/membrane-api.json get /connections/{elementSelector}/dependencies # List all connections Source: https://docs.getmembrane.com/api-reference/connections/list-all-connections /api-reference/membrane-api.json get /connections # Refresh connection credentials Source: https://docs.getmembrane.com/api-reference/connections/refresh-connection-credentials /api-reference/membrane-api.json post /connections/{id}/refresh-credentials # Restore (unarchive) connection Source: https://docs.getmembrane.com/api-reference/connections/restore-unarchive-connection /api-reference/membrane-api.json post /connections/{elementSelector}/restore # Test connection validity Source: https://docs.getmembrane.com/api-reference/connections/test-connection-validity /api-reference/membrane-api.json post /connections/{id}/test # Update connection completely Source: https://docs.getmembrane.com/api-reference/connections/update-connection-completely /api-reference/membrane-api.json put /connections/{id} # Update connection partially Source: https://docs.getmembrane.com/api-reference/connections/update-connection-partially /api-reference/membrane-api.json patch /connections/{id} # Clone connector to workspace Source: https://docs.getmembrane.com/api-reference/connectors/clone-connector-to-workspace /api-reference/membrane-api.json post /connectors/{idOrKey}/clone # Create a new connector Source: https://docs.getmembrane.com/api-reference/connectors/create-a-new-connector /api-reference/membrane-api.json post /connectors # Delete a connector Source: https://docs.getmembrane.com/api-reference/connectors/delete-a-connector /api-reference/membrane-api.json delete /connectors/{idOrKey} # Delete connector directory Source: https://docs.getmembrane.com/api-reference/connectors/delete-connector-directory /api-reference/membrane-api.json delete /connectors/{idOrKey}/directories/{path} # Delete connector file Source: https://docs.getmembrane.com/api-reference/connectors/delete-connector-file /api-reference/membrane-api.json delete /connectors/{idOrKey}/files/{path} # Download connector as zip file Source: https://docs.getmembrane.com/api-reference/connectors/download-connector-as-zip-file /api-reference/membrane-api.json get /connectors/{idOrKey}/download # Export connector as JSON Source: https://docs.getmembrane.com/api-reference/connectors/export-connector-as-json /api-reference/membrane-api.json get /connectors/{idOrKey}/export-json # Export connector as zip file Source: https://docs.getmembrane.com/api-reference/connectors/export-connector-as-zip-file /api-reference/membrane-api.json get /connectors/{idOrKey}/export-files # Get a list of connector files Source: https://docs.getmembrane.com/api-reference/connectors/get-a-list-of-connector-files /api-reference/membrane-api.json get /connectors/{idOrKey}/files # Get all versions of a connector Source: https://docs.getmembrane.com/api-reference/connectors/get-all-versions-of-a-connector /api-reference/membrane-api.json get /connectors/{key}/versions # Get connector Source: https://docs.getmembrane.com/api-reference/connectors/get-connector /api-reference/membrane-api.json get /connectors/{idOrKey} # Get connector file by path Source: https://docs.getmembrane.com/api-reference/connectors/get-connector-file-by-path /api-reference/membrane-api.json get /connectors/{idOrKey}/files/{path} # Import connector from external source Source: https://docs.getmembrane.com/api-reference/connectors/import-connector-from-external-source /api-reference/membrane-api.json post /connectors/{idOrKey}/import # List all connectors Source: https://docs.getmembrane.com/api-reference/connectors/list-all-connectors /api-reference/membrane-api.json get /connectors # Publish a new version of connector Source: https://docs.getmembrane.com/api-reference/connectors/publish-a-new-version-of-connector /api-reference/membrane-api.json post /connectors/{key}/publish-version # Update connector completely Source: https://docs.getmembrane.com/api-reference/connectors/update-connector-completely /api-reference/membrane-api.json put /connectors/{idOrKey} # Update connector file Source: https://docs.getmembrane.com/api-reference/connectors/update-connector-file /api-reference/membrane-api.json put /connectors/{idOrKey}/files/{path} # Update connector partially Source: https://docs.getmembrane.com/api-reference/connectors/update-connector-partially /api-reference/membrane-api.json patch /connectors/{idOrKey} # Upload connector from zip file Source: https://docs.getmembrane.com/api-reference/connectors/upload-connector-from-zip-file /api-reference/membrane-api.json post /connectors/{idOrKey}/upload # List public Membrane connectors Source: https://docs.getmembrane.com/api-reference/connectorsuniverse/list-public-membrane-connectors /api-reference/membrane-api.json get /universe/connectors # Create a new customer Source: https://docs.getmembrane.com/api-reference/customers/create-a-new-customer /api-reference/membrane-api.json post /customers # Delete a customer Source: https://docs.getmembrane.com/api-reference/customers/delete-a-customer /api-reference/membrane-api.json delete /customers/{id} # Get customer by ID Source: https://docs.getmembrane.com/api-reference/customers/get-customer-by-id /api-reference/membrane-api.json get /customers/{id} # List all customers Source: https://docs.getmembrane.com/api-reference/customers/list-all-customers /api-reference/membrane-api.json get /customers # Restore (unarchive) customer Source: https://docs.getmembrane.com/api-reference/customers/restore-unarchive-customer /api-reference/membrane-api.json post /customers/{elementSelector}/restore # Update customer completely Source: https://docs.getmembrane.com/api-reference/customers/update-customer-completely /api-reference/membrane-api.json put /customers/{id} # Update customer partially Source: https://docs.getmembrane.com/api-reference/customers/update-customer-partially /api-reference/membrane-api.json patch /customers/{id} # Create record in data collection Source: https://docs.getmembrane.com/api-reference/datacollections/create-record-in-data-collection /api-reference/membrane-api.json post /connections/{id}/data/{dataCollectionKey}/create # Delete record in data collection Source: https://docs.getmembrane.com/api-reference/datacollections/delete-record-in-data-collection /api-reference/membrane-api.json post /connections/{id}/data/{dataCollectionKey}/delete # Find a matching record in a data collection Source: https://docs.getmembrane.com/api-reference/datacollections/find-a-matching-record-in-a-data-collection /api-reference/membrane-api.json post /connections/{id}/data/{dataCollectionKey}/match # Find record by ID in data collection Source: https://docs.getmembrane.com/api-reference/datacollections/find-record-by-id-in-data-collection /api-reference/membrane-api.json post /connections/{id}/data/{dataCollectionKey}/find-by-id # Get data collection specification Source: https://docs.getmembrane.com/api-reference/datacollections/get-data-collection-specification /api-reference/membrane-api.json get /connections/{id}/data/{dataCollectionKey} # List connection data collections Source: https://docs.getmembrane.com/api-reference/datacollections/list-connection-data-collections /api-reference/membrane-api.json get /connections/{id}/data # List records in data collection Source: https://docs.getmembrane.com/api-reference/datacollections/list-records-in-data-collection /api-reference/membrane-api.json post /connections/{id}/data/{dataCollectionKey}/list # Refresh data collection specification Source: https://docs.getmembrane.com/api-reference/datacollections/refresh-data-collection-specification /api-reference/membrane-api.json post /connections/{id}/data/{dataCollectionKey}/refresh # Search records in a data collection Source: https://docs.getmembrane.com/api-reference/datacollections/search-records-in-a-data-collection /api-reference/membrane-api.json post /connections/{id}/data/{dataCollectionKey}/search # Update record in data collection Source: https://docs.getmembrane.com/api-reference/datacollections/update-record-in-data-collection /api-reference/membrane-api.json post /connections/{id}/data/{dataCollectionKey}/update # Delete data link Source: https://docs.getmembrane.com/api-reference/datalinks/delete-data-link /api-reference/membrane-api.json delete /data-links/{dataLinkId} # List data links Source: https://docs.getmembrane.com/api-reference/datalinks/list-data-links /api-reference/membrane-api.json get /data-links # Archive data link table Source: https://docs.getmembrane.com/api-reference/datalinktables/archive-data-link-table /api-reference/membrane-api.json delete /data-link-tables/{idOrKey} # Create a data link Source: https://docs.getmembrane.com/api-reference/datalinktables/create-a-data-link /api-reference/membrane-api.json post /data-link-tables/{idOrKey}/links # Create data link table Source: https://docs.getmembrane.com/api-reference/datalinktables/create-data-link-table /api-reference/membrane-api.json post /data-link-tables # Delete a data link Source: https://docs.getmembrane.com/api-reference/datalinktables/delete-a-data-link /api-reference/membrane-api.json delete /data-link-tables/{idOrKey}/links # Export data link table Source: https://docs.getmembrane.com/api-reference/datalinktables/export-data-link-table /api-reference/membrane-api.json get /data-link-tables/{idOrKey}/export # Get data link table Source: https://docs.getmembrane.com/api-reference/datalinktables/get-data-link-table /api-reference/membrane-api.json get /data-link-tables/{idOrKey} # Get data links for a data link table instance Source: https://docs.getmembrane.com/api-reference/datalinktables/get-data-links-for-a-data-link-table-instance /api-reference/membrane-api.json get /data-link-tables/{idOrKey}/links # Get dependencies of a data link table Source: https://docs.getmembrane.com/api-reference/datalinktables/get-dependencies-of-a-data-link-table /api-reference/membrane-api.json get /data-link-tables/{elementSelector}/dependencies # List data link tables Source: https://docs.getmembrane.com/api-reference/datalinktables/list-data-link-tables /api-reference/membrane-api.json get /data-link-tables # Patch data link table Source: https://docs.getmembrane.com/api-reference/datalinktables/patch-data-link-table /api-reference/membrane-api.json patch /data-link-tables/{idOrKey} # Refresh data link table Source: https://docs.getmembrane.com/api-reference/datalinktables/refresh-data-link-table /api-reference/membrane-api.json post /data-link-tables/{elementSelector}/refresh # Restore (unarchive) data link table Source: https://docs.getmembrane.com/api-reference/datalinktables/restore-unarchive-data-link-table /api-reference/membrane-api.json post /data-link-tables/{elementSelector}/restore # Update data link table Source: https://docs.getmembrane.com/api-reference/datalinktables/update-data-link-table /api-reference/membrane-api.json put /data-link-tables/{idOrKey} # Archive data source Source: https://docs.getmembrane.com/api-reference/datasources/archive-data-source /api-reference/membrane-api.json delete /data-sources/{selector} # Clone data source Source: https://docs.getmembrane.com/api-reference/datasources/clone-data-source /api-reference/membrane-api.json post /data-sources/{selector}/clone # Create a new data source Source: https://docs.getmembrane.com/api-reference/datasources/create-a-new-data-source /api-reference/membrane-api.json post /data-sources # Export data source (flat interface) Source: https://docs.getmembrane.com/api-reference/datasources/export-data-source-flat-interface /api-reference/membrane-api.json get /data-sources/{selector}/export # Get data source by selector Source: https://docs.getmembrane.com/api-reference/datasources/get-data-source-by-selector /api-reference/membrane-api.json get /data-sources/{selector} # Get dependencies of data source (flat interface) Source: https://docs.getmembrane.com/api-reference/datasources/get-dependencies-of-data-source-flat-interface /api-reference/membrane-api.json get /data-sources/{selector}/dependencies # List data sources Source: https://docs.getmembrane.com/api-reference/datasources/list-data-sources /api-reference/membrane-api.json get /data-sources # Refresh data source Source: https://docs.getmembrane.com/api-reference/datasources/refresh-data-source /api-reference/membrane-api.json post /data-sources/{selector}/refresh # Replace data source completely Source: https://docs.getmembrane.com/api-reference/datasources/replace-data-source-completely /api-reference/membrane-api.json put /data-sources/{selector} # Reset data source Source: https://docs.getmembrane.com/api-reference/datasources/reset-data-source /api-reference/membrane-api.json post /data-sources/{selector}/reset Refreshes all the dependencies, then resets the element to the default state (from parent) # Restore data source (flat interface) Source: https://docs.getmembrane.com/api-reference/datasources/restore-data-source-flat-interface /api-reference/membrane-api.json post /data-sources/{selector}/restore # Update data source partially Source: https://docs.getmembrane.com/api-reference/datasources/update-data-source-partially /api-reference/membrane-api.json patch /data-sources/{selector} # Export all workspace elements Source: https://docs.getmembrane.com/api-reference/exportmembraneinterfaces/export-all-workspace-elements /api-reference/membrane-api.json get /export # Get API log by ID Source: https://docs.getmembrane.com/api-reference/externalapilogs/get-api-log-by-id /api-reference/membrane-api.json get /external-api-logs/{id} # Get API log content Source: https://docs.getmembrane.com/api-reference/externalapilogs/get-api-log-content /api-reference/membrane-api.json get /external-api-logs/{id}/details # List external API logs Source: https://docs.getmembrane.com/api-reference/externalapilogs/list-external-api-logs /api-reference/membrane-api.json get /external-api-logs # Create a new app Source: https://docs.getmembrane.com/api-reference/externalapps/create-a-new-app /api-reference/membrane-api.json post /external-apps # Delete an app Source: https://docs.getmembrane.com/api-reference/externalapps/delete-an-app /api-reference/membrane-api.json delete /external-apps/{idOrKeyOrUuid} # Get a single app Source: https://docs.getmembrane.com/api-reference/externalapps/get-a-single-app /api-reference/membrane-api.json get /external-apps/{idOrKeyOrUuid} Returns a public app by its ID, key, or UUID with OpenAPI specs. # List apps Source: https://docs.getmembrane.com/api-reference/externalapps/list-apps /api-reference/membrane-api.json get /external-apps Returns workspace apps first, then public apps. # Update an app Source: https://docs.getmembrane.com/api-reference/externalapps/update-an-app /api-reference/membrane-api.json patch /external-apps/{idOrKeyOrUuid} # Get external event log record Source: https://docs.getmembrane.com/api-reference/externaleventlogrecords/get-external-event-log-record /api-reference/membrane-api.json get /external-event-log-records/{id} # Get external event log record details Source: https://docs.getmembrane.com/api-reference/externaleventlogrecords/get-external-event-log-record-details /api-reference/membrane-api.json get /external-event-log-records/{id}/details # List external event log records Source: https://docs.getmembrane.com/api-reference/externaleventlogrecords/list-external-event-log-records /api-reference/membrane-api.json get /external-event-log-records # Get external event pull Source: https://docs.getmembrane.com/api-reference/externaleventpulls/get-external-event-pull /api-reference/membrane-api.json get /external-event-pulls/{id} # Get external event pull logs Source: https://docs.getmembrane.com/api-reference/externaleventpulls/get-external-event-pull-logs /api-reference/membrane-api.json get /external-event-pulls/{id}/logs # List external event pulls Source: https://docs.getmembrane.com/api-reference/externaleventpulls/list-external-event-pulls /api-reference/membrane-api.json get /external-event-pulls # Delete external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/delete-external-event-subscription /api-reference/membrane-api.json delete /external-event-subscriptions/{id} # Download full sync snapshot for external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/download-full-sync-snapshot-for-external-event-subscription /api-reference/membrane-api.json post /external-event-subscriptions/{id}/download-full-sync-snapshot # Get dependencies of a external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/get-dependencies-of-a-external-event-subscription /api-reference/membrane-api.json get /external-event-subscriptions/{elementSelector}/dependencies # Get external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/get-external-event-subscription /api-reference/membrane-api.json get /external-event-subscriptions/{id} # List external event subscriptions Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/list-external-event-subscriptions /api-reference/membrane-api.json get /external-event-subscriptions # Refresh external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/refresh-external-event-subscription /api-reference/membrane-api.json post /external-event-subscriptions/{elementSelector}/refresh # Restore (unarchive) external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/restore-unarchive-external-event-subscription /api-reference/membrane-api.json post /external-event-subscriptions/{elementSelector}/restore # Resubscribe to external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/resubscribe-to-external-event-subscription /api-reference/membrane-api.json post /external-event-subscriptions/{id}/resubscribe # Setup external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/setup-external-event-subscription /api-reference/membrane-api.json post /external-event-subscriptions/{id}/setup # Subscribe to external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/subscribe-to-external-event-subscription /api-reference/membrane-api.json post /external-event-subscriptions/{id}/subscribe # Triggers pull events for external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/triggers-pull-events-for-external-event-subscription /api-reference/membrane-api.json post /external-event-subscriptions/{id}/pull-events # Unsubscribe from external event subscription Source: https://docs.getmembrane.com/api-reference/externaleventsubscriptions/unsubscribe-from-external-event-subscription /api-reference/membrane-api.json post /external-event-subscriptions/{id}/unsubscribe # Archive field mapping Source: https://docs.getmembrane.com/api-reference/fieldmappings/archive-field-mapping /api-reference/membrane-api.json delete /field-mappings/{selector} # Clone field mapping Source: https://docs.getmembrane.com/api-reference/fieldmappings/clone-field-mapping /api-reference/membrane-api.json post /field-mappings/{selector}/clone # Create a new field mapping Source: https://docs.getmembrane.com/api-reference/fieldmappings/create-a-new-field-mapping /api-reference/membrane-api.json post /field-mappings # Export field mapping (flat interface) Source: https://docs.getmembrane.com/api-reference/fieldmappings/export-field-mapping-flat-interface /api-reference/membrane-api.json get /field-mappings/{selector}/export # Get dependencies of field mapping (flat interface) Source: https://docs.getmembrane.com/api-reference/fieldmappings/get-dependencies-of-field-mapping-flat-interface /api-reference/membrane-api.json get /field-mappings/{selector}/dependencies # Get field mapping app schema Source: https://docs.getmembrane.com/api-reference/fieldmappings/get-field-mapping-app-schema /api-reference/membrane-api.json get /field-mappings/{selector}/app-schema # Get field mapping by selector Source: https://docs.getmembrane.com/api-reference/fieldmappings/get-field-mapping-by-selector /api-reference/membrane-api.json get /field-mappings/{selector} # List field mappings Source: https://docs.getmembrane.com/api-reference/fieldmappings/list-field-mappings /api-reference/membrane-api.json get /field-mappings # Refresh field mapping Source: https://docs.getmembrane.com/api-reference/fieldmappings/refresh-field-mapping /api-reference/membrane-api.json post /field-mappings/{selector}/refresh # Replace field mapping completely Source: https://docs.getmembrane.com/api-reference/fieldmappings/replace-field-mapping-completely /api-reference/membrane-api.json put /field-mappings/{selector} # Reset field mapping Source: https://docs.getmembrane.com/api-reference/fieldmappings/reset-field-mapping /api-reference/membrane-api.json post /field-mappings/{selector}/reset Refreshes all the dependencies, then resets the element to the default state (from parent) # Restore field mapping (flat interface) Source: https://docs.getmembrane.com/api-reference/fieldmappings/restore-field-mapping-flat-interface /api-reference/membrane-api.json post /field-mappings/{selector}/restore # Update field mapping partially Source: https://docs.getmembrane.com/api-reference/fieldmappings/update-field-mapping-partially /api-reference/membrane-api.json patch /field-mappings/{selector} # Upload a file Source: https://docs.getmembrane.com/api-reference/files/upload-a-file /api-reference/membrane-api.json post /files # Get flow run by ID Source: https://docs.getmembrane.com/api-reference/flowruns/get-flow-run-by-id /api-reference/membrane-api.json get /flow-runs/{id} # Get flow run output Source: https://docs.getmembrane.com/api-reference/flowruns/get-flow-run-output /api-reference/membrane-api.json get /flow-runs/{id}/output # Get flow run output for specific node Source: https://docs.getmembrane.com/api-reference/flowruns/get-flow-run-output-for-specific-node /api-reference/membrane-api.json get /flow-runs/{id}/output/{nodeKey} # Get node run outputs Source: https://docs.getmembrane.com/api-reference/flowruns/get-node-run-outputs /api-reference/membrane-api.json get /flow-runs/{id}/nodes/{nodeKey}/outputs # Get node runs for flow run Source: https://docs.getmembrane.com/api-reference/flowruns/get-node-runs-for-flow-run /api-reference/membrane-api.json get /flow-runs/{id}/nodes/{nodeKey}/runs # Get specific node run Source: https://docs.getmembrane.com/api-reference/flowruns/get-specific-node-run /api-reference/membrane-api.json get /flow-runs/{id}/nodes/{nodeKey}/runs/{nodeRunId} # Get specific node run output Source: https://docs.getmembrane.com/api-reference/flowruns/get-specific-node-run-output /api-reference/membrane-api.json get /flow-runs/{id}/nodes/{nodeKey}/outputs/{outputId} # List all flow runs Source: https://docs.getmembrane.com/api-reference/flowruns/list-all-flow-runs /api-reference/membrane-api.json get /flow-runs # Stop flow run execution Source: https://docs.getmembrane.com/api-reference/flowruns/stop-flow-run-execution /api-reference/membrane-api.json post /flow-runs/{id}/stop # Archive flow Source: https://docs.getmembrane.com/api-reference/flows/archive-flow /api-reference/membrane-api.json delete /flows/{selector} # Clone flow Source: https://docs.getmembrane.com/api-reference/flows/clone-flow /api-reference/membrane-api.json post /flows/{selector}/clone # Create a new flow Source: https://docs.getmembrane.com/api-reference/flows/create-a-new-flow /api-reference/membrane-api.json post /flows # Export flow (flat interface) Source: https://docs.getmembrane.com/api-reference/flows/export-flow-flat-interface /api-reference/membrane-api.json get /flows/{selector}/export # Get dependencies of flow (flat interface) Source: https://docs.getmembrane.com/api-reference/flows/get-dependencies-of-flow-flat-interface /api-reference/membrane-api.json get /flows/{selector}/dependencies # Get flow by selector Source: https://docs.getmembrane.com/api-reference/flows/get-flow-by-selector /api-reference/membrane-api.json get /flows/{selector} # List flows Source: https://docs.getmembrane.com/api-reference/flows/list-flows /api-reference/membrane-api.json get /flows # Refresh flow Source: https://docs.getmembrane.com/api-reference/flows/refresh-flow /api-reference/membrane-api.json post /flows/{selector}/refresh # Replace flow completely Source: https://docs.getmembrane.com/api-reference/flows/replace-flow-completely /api-reference/membrane-api.json put /flows/{selector} # Reset flow Source: https://docs.getmembrane.com/api-reference/flows/reset-flow /api-reference/membrane-api.json post /flows/{selector}/reset Refreshes all the dependencies, then resets the element to the default state (from parent) # Restore flow (flat interface) Source: https://docs.getmembrane.com/api-reference/flows/restore-flow-flat-interface /api-reference/membrane-api.json post /flows/{selector}/restore # Update flow partially Source: https://docs.getmembrane.com/api-reference/flows/update-flow-partially /api-reference/membrane-api.json patch /flows/{selector} # Import workspace elements from membrane zip file Source: https://docs.getmembrane.com/api-reference/importmembraneinterfaces/import-workspace-elements-from-membrane-zip-file /api-reference/membrane-api.json post /import # Get incoming webhook by ID Source: https://docs.getmembrane.com/api-reference/incomingwebhooksreader/get-incoming-webhook-by-id /api-reference/membrane-api.json get /incoming-webhooks/{id} # Get incoming webhook details Source: https://docs.getmembrane.com/api-reference/incomingwebhooksreader/get-incoming-webhook-details /api-reference/membrane-api.json get /incoming-webhooks/{id}/details # List incoming webhooks Source: https://docs.getmembrane.com/api-reference/incomingwebhooksreader/list-incoming-webhooks /api-reference/membrane-api.json get /incoming-webhooks # Archive an integration Source: https://docs.getmembrane.com/api-reference/integrations/archive-an-integration /api-reference/membrane-api.json delete /integrations/{id} # Create a new integration Source: https://docs.getmembrane.com/api-reference/integrations/create-a-new-integration /api-reference/membrane-api.json post /integrations # Export integration by ID Source: https://docs.getmembrane.com/api-reference/integrations/export-integration-by-id /api-reference/membrane-api.json get /integrations/{id}/export # Get integration by selector (id, uuid, or key) Source: https://docs.getmembrane.com/api-reference/integrations/get-integration-by-selector-id-uuid-or-key /api-reference/membrane-api.json get /integrations/{idOrKey} # Get integration data location Source: https://docs.getmembrane.com/api-reference/integrations/get-integration-data-location /api-reference/membrane-api.json get /integrations/{id}/data/{dataLocationKey} # Get integration parameters Source: https://docs.getmembrane.com/api-reference/integrations/get-integration-parameters /api-reference/membrane-api.json get /integrations/{id}/parameters # List integration data locations Source: https://docs.getmembrane.com/api-reference/integrations/list-integration-data-locations /api-reference/membrane-api.json get /integrations/{id}/data # List integration global webhooks Source: https://docs.getmembrane.com/api-reference/integrations/list-integration-global-webhooks /api-reference/membrane-api.json get /integrations/{id}/global-webhooks # Refresh integration Source: https://docs.getmembrane.com/api-reference/integrations/refresh-integration /api-reference/membrane-api.json post /integrations/{elementSelector}/refresh # Restore (unarchive) integration Source: https://docs.getmembrane.com/api-reference/integrations/restore-unarchive-integration /api-reference/membrane-api.json post /integrations/{elementSelector}/restore # Set integration parameters Source: https://docs.getmembrane.com/api-reference/integrations/set-integration-parameters /api-reference/membrane-api.json put /integrations/{idOrKey}/parameters # Setup integration configuration Source: https://docs.getmembrane.com/api-reference/integrations/setup-integration-configuration /api-reference/membrane-api.json post /integrations/{id}/setup # Update integration completely Source: https://docs.getmembrane.com/api-reference/integrations/update-integration-completely /api-reference/membrane-api.json put /integrations/{id} # Update integration partially Source: https://docs.getmembrane.com/api-reference/integrations/update-integration-partially /api-reference/membrane-api.json patch /integrations/{id} # Upload connector to integration Source: https://docs.getmembrane.com/api-reference/integrations/upload-connector-to-integration /api-reference/membrane-api.json post /integrations/{id}/upload-connector # Archive an organization workspace Source: https://docs.getmembrane.com/api-reference/orgworkspaces/archive-an-organization-workspace /api-reference/membrane-api.json delete /org-workspaces/{id} # Create a workspace in an organization Source: https://docs.getmembrane.com/api-reference/orgworkspaces/create-a-workspace-in-an-organization /api-reference/membrane-api.json post /org-workspaces # Generate an access token for an organization workspace Source: https://docs.getmembrane.com/api-reference/orgworkspaces/generate-an-access-token-for-an-organization-workspace /api-reference/membrane-api.json post /org-workspaces/{id}/access-token # Get an organization workspace Source: https://docs.getmembrane.com/api-reference/orgworkspaces/get-an-organization-workspace /api-reference/membrane-api.json get /org-workspaces/{id} # Restore an archived organization workspace Source: https://docs.getmembrane.com/api-reference/orgworkspaces/restore-an-archived-organization-workspace /api-reference/membrane-api.json post /org-workspaces/{id}/restore # Rotate the secret for an organization workspace Source: https://docs.getmembrane.com/api-reference/orgworkspaces/rotate-the-secret-for-an-organization-workspace /api-reference/membrane-api.json post /org-workspaces/{id}/rotate-secret # Update an organization workspace Source: https://docs.getmembrane.com/api-reference/orgworkspaces/update-an-organization-workspace /api-reference/membrane-api.json patch /org-workspaces/{id} # Archive package Source: https://docs.getmembrane.com/api-reference/packages/archive-package /api-reference/membrane-api.json delete /packages/{selector} # Clone package Source: https://docs.getmembrane.com/api-reference/packages/clone-package /api-reference/membrane-api.json post /packages/{selector}/clone # Create a new package Source: https://docs.getmembrane.com/api-reference/packages/create-a-new-package /api-reference/membrane-api.json post /packages # Export package (flat interface) Source: https://docs.getmembrane.com/api-reference/packages/export-package-flat-interface /api-reference/membrane-api.json get /packages/{selector}/export # Get all versions of a package Source: https://docs.getmembrane.com/api-reference/packages/get-all-versions-of-a-package /api-reference/membrane-api.json get /packages/{id}/versions # Get dependencies of package (flat interface) Source: https://docs.getmembrane.com/api-reference/packages/get-dependencies-of-package-flat-interface /api-reference/membrane-api.json get /packages/{selector}/dependencies # Get package by selector Source: https://docs.getmembrane.com/api-reference/packages/get-package-by-selector /api-reference/membrane-api.json get /packages/{selector} # List packages Source: https://docs.getmembrane.com/api-reference/packages/list-packages /api-reference/membrane-api.json get /packages # Refresh package Source: https://docs.getmembrane.com/api-reference/packages/refresh-package /api-reference/membrane-api.json post /packages/{selector}/refresh # Replace package completely Source: https://docs.getmembrane.com/api-reference/packages/replace-package-completely /api-reference/membrane-api.json put /packages/{selector} # Reset package Source: https://docs.getmembrane.com/api-reference/packages/reset-package /api-reference/membrane-api.json post /packages/{selector}/reset Refreshes all the dependencies, then resets the element to the default state (from parent) # Restore package (flat interface) Source: https://docs.getmembrane.com/api-reference/packages/restore-package-flat-interface /api-reference/membrane-api.json post /packages/{selector}/restore # Update package partially Source: https://docs.getmembrane.com/api-reference/packages/update-package-partially /api-reference/membrane-api.json patch /packages/{selector} # List public packages Source: https://docs.getmembrane.com/api-reference/packagesuniverse/list-public-packages /api-reference/membrane-api.json get /universe/packages # Run a flow Source: https://docs.getmembrane.com/api-reference/runflow/run-a-flow /api-reference/membrane-api.json post /flows/{selector}/run # Create a new agent session Source: https://docs.getmembrane.com/api-reference/sessions/create-a-new-agent-session /api-reference/membrane-api.json post /agent/sessions # Get an agent session Source: https://docs.getmembrane.com/api-reference/sessions/get-an-agent-session /api-reference/membrane-api.json get /agent/sessions/{id} # Interrupt a running agent session Source: https://docs.getmembrane.com/api-reference/sessions/interrupt-a-running-agent-session /api-reference/membrane-api.json post /agent/sessions/{id}/interrupt # List agent sessions Source: https://docs.getmembrane.com/api-reference/sessions/list-agent-sessions /api-reference/membrane-api.json get /agent/sessions # Send a message to a session (resumes if stopped, sends input if running) Source: https://docs.getmembrane.com/api-reference/sessions/send-a-message-to-a-session-resumes-if-stopped-sends-input-if-running /api-reference/membrane-api.json post /agent/sessions/{id}/message # Stop an agent session Source: https://docs.getmembrane.com/api-reference/sessions/stop-an-agent-session /api-reference/membrane-api.json patch /agent/sessions/{id}/stop # Update an agent session Source: https://docs.getmembrane.com/api-reference/sessions/update-an-agent-session /api-reference/membrane-api.json patch /agent/sessions/{id} # Search workspace elements by keyword Source: https://docs.getmembrane.com/api-reference/workspaceelementssearch/search-workspace-elements-by-keyword /api-reference/membrane-api.json get /search # Search workspace elements using semantic similarity Source: https://docs.getmembrane.com/api-reference/workspaceelementssearch/search-workspace-elements-using-semantic-similarity /api-reference/membrane-api.json get /semantic-search # Overview Source: https://docs.getmembrane.com/docs/getting-started/overview Membrane makes your software talk to anything else. This documentation describes how. ## Getting Started To get started, pick your use case: Give Claude, ChatGPT, Cursor, or any agent the ability to connect to apps, run actions, and react to events. Add integrations to the software you build so its users could connect external apps, sync data, and automate workflows. ## Ways to Use Membrane Membrane is fully programmable and API-first. All functionality is accessible through multiple interfaces: Give AI agents context and tools to talk to anything. Authenticate, manage, and run your integrations in a command line. Call any Membrane functionality via API endpoints. Use Membrane via TypeScript/JavaScript SDK with typed accessors. Use Membrane via Model Context Protocol. Pre-built UI components for integrating your software. Web UI for managing and monitoring your Membrane workspace. ## How Membrane Works All interactions with external apps fall into three main categories: **Connect** — Authenticate with an external app (OAuth, API key, etc.) to be able to talk to it. This creates a [Connection](/docs/how-membrane-works/connections) — an authenticated link that Membrane keeps alive, refreshes tokens for, and reconnects on failure. **Act** — Take [Actions](/docs/how-membrane-works/actions) in the connected app: create a contact, send a message, list invoices, update a deal. Actions run on top of connections and use them for authentication. Actions can be anything from a simple API request to a complex multi-step workflow. **React** — Subscribe to changes in the connected app via [Events](/docs/how-membrane-works/events). Membrane detects new records, updates, and deletions via webhooks or polling, and delivers them to your backend. ### Multi-tenancy If you're using or building a product where multiple people connect their own apps, use [Tenants](/docs/how-membrane-works/tenants) to isolate their connections and data. Each tenant has their own list of connections and a full set of integration settings. Tenants cannot access each other's data. Each tenant is identified by `tenantKey` - an arbitrary string you provide that usually corresponds to a unique identifier on your side. Membrane knows which tenant to use for each interaction based on Membrane Token you generate. See [Authentication](/docs/how-membrane-works/authentication) for details. ### Membrane Layers Membrane elements like actions and events can be defined at three levels of specificity: | Layer | Scope | Example | | - | - | - | | **Universal** | Works across any app | "Create Contact" action that works with any CRM | | **Integration** | One specific app | How "Create Contact" maps to HubSpot's API — an *implementation* of the universal element | | **Connection** | One specific connection | Custom field mapping for a particular HubSpot account — a *customization* of the integration element | This is useful when you want to create standard integration logic that works across many connections or apps and only define differences for each app/connection when they appear. This allows your code operate on abstract layer and let Membrane resolve it to the correct configuration for a given app and connection. ## Managing Membrane Logs, alerts, error inspection, and debugging tools for every integration touchpoint. Deploy Membrane on your own infrastructure with Docker and Helm charts. Move workspace configuration between environments. Security practices, data privacy, and compliance. # Quickstart: Product Integrations Source: https://docs.getmembrane.com/docs/getting-started/product-integrations Build integration features into your product so your tenants can connect their apps, sync data, and automate workflows. ## 1. Install Membrane skill for building product integrations ```bash theme={null} npx skills add membranehq/agent-skills --skill integration-development ``` This gives your coding agent the context and tools specifically for building integration features into your product. ## 2. Ask your agent to build what you need > "Build an integrations page where my users can connect to HubSpot, Salesforce, and Slack." > > "Add a data sync that imports contacts from connected CRMs into our database." > > "Give our AI assistant tools from our users' connected apps." Your agent will use the [CLI](/docs/ways-to-use-membrane/cli) to interact with Membrane (creating connections, configuring actions, testing) and will use the [SDK](/docs/ways-to-use-membrane/sdk), [REST API](/docs/ways-to-use-membrane/rest-api), and [Embedded UI](/docs/ways-to-use-membrane/embedded-ui) to add integration features to your app. ## 3. Monitor and troubleshoot in the Console Use the [Membrane Console](https://console.getmembrane.com) to review what your agent built — inspect connections, actions, and flows. When something doesn't work as expected, dig into the details down to individual API requests in the [Activity Log](https://console.getmembrane.com/w/0/activity). ## Use Case Guides * [Integration Catalog](/docs/getting-started/product-integrations/integration-catalog) — build an integrations page with connections management * [In-app AI Agent](/docs/getting-started/product-integrations/in-app-ai-agent) — give your product's AI agent tools from connected apps * [In-app Workflow Builder](/docs/getting-started/product-integrations/in-app-workflow-builder) — let users build automation workflows * [Data Integrations](/docs/getting-started/product-integrations/data-integrations) — sync data between your product and external apps # Data Integrations Source: https://docs.getmembrane.com/docs/getting-started/product-integrations/data-integrations Move data between your app and your tenants' external apps — import, export, or keep it synchronized in both directions. Your agent can set up and manage data sync configurations, while Membrane handles authentication, pagination, data transformation, and change detection. ## Getting Started 1. Complete the [Quickstart: Product Integrations](/docs/getting-started/product-integrations) guide. 2. Ask your coding agent to build the data integration: > "Set up a continuous data sync that imports contacts from connected CRMs into our database and keeps them up to date." If you want to learn how it works under the hood — read on. ## Common Patterns Import data from external apps and keep it continuously synchronized. Send data from your app to external systems like CRMs or databases. Keep data synchronized in both directions between your app and external apps. Let users choose which data collections to import from external apps. ## How It Works 1. **Define data sources** — Create [Data Sources](/reference/workspace-elements/data-sources) that map to the types of data you want to sync (contacts, companies, tasks, etc.). 2. **Configure field mappings** — Use [Field Mappings](/reference/workspace-elements/field-mappings) to define how fields in external apps correspond to fields in your app. 3. **Set up sync flows** — Create [Flows](/reference/workspace-elements/flows) that handle initial imports, incremental updates, and change events. 4. **Membrane handles the rest** — Authentication, pagination, data transformation, and event subscriptions are managed automatically. ## Key Building Blocks * **[Data Sources](/reference/workspace-elements/data-sources)** — define what data to sync and where it lives in external apps * **[Field Mappings](/reference/workspace-elements/field-mappings)** — map fields between your schema and external app schemas; let tenants customize mappings * **[Data Links](/reference/workspace-elements/data-links)** — track relationships between records in your app and records in external apps * **[Flows](/reference/workspace-elements/flows)** — orchestrate multi-step sync workflows with triggers, actions, and conditional logic # Bi-directional Data Sync Source: https://docs.getmembrane.com/docs/getting-started/product-integrations/data-integrations/bi-directional-data-sync There are two main types of bi-directional data sync: * You push data to an external app and then reflect updates back to your app. For example, you create tasks in an external task manager and then refresh their status in your app. * You import data from an external app and let your users update it, with updates being reflected in the original app. This solution can be assembled from the following components: * [Continuous Data Import](/docs/getting-started/product-integrations/data-integrations/continuous-data-import) * [Pushing Data to External Apps](/docs/getting-started/product-integrations/data-integrations/pushing-data-to-external-apps) Additionally, you may want to keep track of relations between objects in your app and corresponding objects in the external app using [Data Links](/reference/workspace-elements/data-links). Alternatively, you can keep these relations in your app's database. Here is how to assemble the full bi-directional sync depending on your use case: # Continuous Import Full solution: [Continuous Import](/docs/getting-started/product-integrations/data-integrations/continuous-data-import) If you use the external app as the source of truth, you should implement the full Continuous Import scenario. If you only care about the data originated in your app, skip the full import and only implement the part that receives updates. Additionally, if you keep track of data links using Membrane, you will need to create links when importing records. See details below. # Data Push Full solution: [Pushing Data to External Apps](/docs/getting-started/product-integrations/data-integrations/pushing-data-to-external-apps) If you use the external app as a source of truth, you may skip the "Lookup Data Record" step and always create new records in the external app when a new record is created in your app (to avoid creating multiple records in your app that match the same external record). If you keep track of data links using Membrane, you will need to find a matching external record id when reacting to updates in your app. # Data Links When reacting to changes in the external app, you need to know which record in your app to update—and the other way around. You can keep these links in your app's database, or use [Data Links](/reference/workspace-elements/data-links). If you choose the latter, you will need to implement the following pieces of logic, depending on your use case: * When performing a full import of records from an external app, use [Data Link API](/reference/workspace-elements/data-links/data-link-table-instances) to find or create links between records in your app and the external app. * When receiving updates from the external app, find the corresponding record using the data link and send the found record id alongside the changed record to your app. * When sending updates to the external app, find the corresponding external record using the data link and use this ID to update or delete the record in the external app (or create a new one if a matching ID was not found). Here is how a simple flow that uses data links to push changes from your app to an external app looks like: Bi-directional Data Sync illustration # Continuous Data Import Source: https://docs.getmembrane.com/docs/getting-started/product-integrations/data-integrations/continuous-data-import # Continuous Data Import from External Apps This guide shows you how to import data from an external app and keep it up-to-date in your app. To make this guide specific, we will import **Users**, but you can use the same approach for any other type of data. ## Data Source First, create a Universal [Data Source](/reference/workspace-elements/data-sources) that points to Users in every app you want to import users from. Continuous Data Import illustration Or via the API: ```bash theme={null} curl -X POST "https://api.getmembrane.com/data-sources" \ -H "Authorization: Bearer $MEMBRANE_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Users", "key": "users", "udm": "users"}' ``` By selecting the Users data model, you instruct Membrane to automatically pick the most appropriate data collection in each external app this Data Source is used for. ## Field Mapping To keep field mapping configuration in one place and let your tenants change it if needed, create a [Field Mapping](/reference/workspace-elements/field-mappings). Select the Users data source you created in the previous step. You can leverage [Universal Data Models](/docs/references/universal-data-models-ref) to pre-populate mappings for standard fields across all applications. Continuous Data Import illustration Or via the API: ```bash theme={null} curl -X POST "https://api.getmembrane.com/field-mappings" \ -H "Authorization: Bearer $MEMBRANE_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Import Users", "key": "import-users", "dataSourceKey": "users", "direction": "import", "defaultImportValue": { "id": { "$var": "$.id" }, "name": { "$var": "$.fullName" } } }' ``` ## Full Data Import To perform the initial import of all users from the external app (and full re-imports in the future if you need to), use the `list-data-records` action type. Create an action in the Console or via the API: Continuous Data Import illustration ```bash theme={null} curl -X POST "https://api.getmembrane.com/actions" \ -H "Authorization: Bearer $MEMBRANE_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Get All Users", "key": "get-all-users", "type": "list-data-records", "inputSchema": { "type": "object", "properties": { "cursor": { "type": "string" } } }, "config": { "dataSource": { "key": "users" }, "fieldMapping": { "key": "import-users" }, "pagination": { "cursor": { "$var": "$.input.cursor" } } } }' ``` Let's break it down. **Data Source** Select the Users Data Source you created in the first step. You will re-use this data source in the following steps, so it's good to keep it consistent. **Pagination** If an external application has a lot of users, this action will return users page by page. Each response will contain a `cursor` that you can use to fetch the next page. To make it work, you need to: * Add `cursor` to the input schema of the action. * Use `cursor` in the pagination config of the action. **Field Mapping** Select the Import Users field mapping you created in the second step. This will ensure that the data you receive is mapped consistently to your data schema across different blueprints. **Running the import** Run the action via SDK to import all users, handling pagination: ```typescript theme={null} let cursor: string | undefined do { const result = await membrane.action('get-all-users').run({ cursor }, { connectionId: '' }) // result.records is an array of mapped user objects for (const user of result.records) { await saveToMyDatabase(user) // { id, name } } cursor = result.cursor } while (cursor) ``` Or test in the Console: Continuous Data Import illustration ## Incremental Data Import To receive incremental updates from external apps, create a [Flow](/reference/workspace-elements/flows) triggered by change events in the Users data source. The flow will look like this: Continuous Data Import illustration It will be triggered by `data-record-created`, `data-record-updated`, and `data-record-deleted` events in the Users data source.\ Then it will fetch the full user record (except for deleted users) and send them to your application. The step that fetches the user record will look like this: Continuous Data Import illustration It uses the same Data Source and Field Mapping as in the "Get All Users" action above. The step that sends users to your app looks like this: Continuous Data Import illustration It simply sends the user record to the URL specified by you in the body of the POST request. **Testing** To test this flow, apply it to an external app you have an account in, create a connection, and make a change in one of the users: create, update, or delete it.\ You should see a flow run and a request sent to the URL you specified. ## Extending Functionality There are many things you can add to the basic functionality described here: * To configure requests made to your app (add authentication, etc), see [Connecting Your App API](/docs/how-membrane-works/internal-api). * Use [Field Mappings UI](/reference/workspace-elements/field-mappings) to let your users customize how fields are mapped from the external app to your app. * [Internal Data Schemas](/reference/workspace-elements/app-data-schemas) will let you define custom per-tenant field schemas that will be used in the field mapping. * If you want to not just import data, but send updates back, check out [Bi-Directional Sync](/docs/getting-started/product-integrations/data-integrations/bi-directional-data-sync). ## Live Examples * [Continuous Import of Files](https://examples.getmembrane.com/continuous-import-of-files) # Import User-selected Data Collections Source: https://docs.getmembrane.com/docs/getting-started/product-integrations/data-integrations/import-user-selected-data-collections # Import User-selected Data [Continuous Import](/docs/getting-started/product-integrations/data-integrations/continuous-data-import) describes how to import a data collection and keep it in sync when you know ahead of time what data you want to import. This guide will help you import one or more data collections picked by your tenant out of available data collections in an external app. ## Data Source To store which data collection the user selected to import, create a Data Source called "Any Data". Import User-selected Data Collections illustration It will not point to any data collections by default—you'll have to configure it later based on what the user selected. ## Full Import To import all the records from the user-selected collection, create a "Get All Records" action that simply lists records from the Any Data data source without any filtering or mapping. Import User-selected Data Collections illustration ## Change Events To get notified when data records are created, updated, or deleted in the user-selected collection, create a Flow that listens to these events. It looks like this: Import User-selected Data Collections illustration This flow subscribes to `data-record-created`, `data-record-updated`, and `data-record-deleted` events in the Any Data data source, fetches the full record for Created and Updated events (they don't always contain the full record by default), and sends the data to your application. ## Selecting Data Collection To make this work, you need to let your user select a data collection. For this, use metadata from the connector. To let your users select any data collection from any available integration, you can do the following: * Show available integrations to the user: [Integration List](/docs/getting-started/product-integrations/integration-catalog) * Let them connect to the selected integration if they haven't done it yet: [Connection](/docs/ways-to-use-membrane/embedded-ui/connection-ui/custom-connection-ui) * List data collections available in the selected connection and patch the "Any Data" data source with the collection selected by the user: [Data Sources](/reference/workspace-elements/data-sources/data-source-instances) ## Multiple Imports If you need to import not one, but possibly many data collections, you should: * Create a unique ID for each import/data collection. * Use this ID as `instanceKey` when referring to [Data Source Instances](/reference/workspace-elements/data-sources/data-source-instances), [Flow Instances](/reference/workspace-elements/flows), [Action Instances](/docs/how-membrane-works/actions). This will let you create as many individually configured imports per user as you need. ## Collection Metadata If you need to understand the structure of the data collection that is being imported, use [Data Collections](/reference/workspace-elements/data-collections). It will let your application understand: * What parameters the collection has or requires. * What fields are available in the collection. * What is the structure of those fields. * Which other collections this one references. # Pushing Data to External Apps Source: https://docs.getmembrane.com/docs/getting-started/product-integrations/data-integrations/pushing-data-to-external-apps In this guide, we will build a few iterations of the data push scenario, increasing the complexity as we go. To make this guide specific, we will be pushing Contacts to an external app. You can use the same approach for any other type of data. ## Create Data Record If you need to simply create a data record in external applications, create a "Create Data Record" [Action](/docs/how-membrane-works/actions). If you want to create one of the standard data models, you can start with a pre-built blueprint, otherwise create one from scratch: Pushing Data to External Apps illustration Here is what you'll need to configure in the action: * **Input Schema** – specify inputs your application will provide to create the record. * **Data Source** – select where in the external app you want to create a data record. You can use one of our [Universal Data Models](/docs/references/universal-data-models-ref) or create a shared [Data Source](/reference/workspace-elements/data-sources) (more on that later). * **Field Mapping** – map your input schema to the fields of the record you want to create. The resulting action will look like this: Pushing Data to External Apps illustration ## Find or Create If you want to check if a matching data record exists in the external app before creating it, create a [Flow](/docs/how-membrane-works/actions) with three steps: * Trigger – we will use an API trigger for simplicity. * Lookup Data Record – to find an existing data record by one of its fields. * Create Contact – we will re-use the "Create Contact" action we've created in the previous step. The flow will look like this: Pushing Data to External Apps illustration Lookup Data Record will search for an existing contact by email: Pushing Data to External Apps illustration Then it will route the execution depending on whether a contact was found. Create Contact step will use the same action we've created before: Pushing Data to External Apps illustration ## Data Source Since we use data collection in two places (lookup contact and create contact), it makes sense to create a [Data Source](/reference/workspace-elements/data-sources): Pushing Data to External Apps illustration and use it in both places: Pushing Data to External Apps illustration ## Field Mapping Similarly to Data Source, it may be a good idea to create a [Field Mapping](/reference/workspace-elements/field-mappings). It will let you: * Use the same mapping in multiple places (i.e. if you want to update contact if it exists, not just create a new one). * Let your tenants configure the field mapping for their unique setup. If you decide to create a Field Mapping, use it in the Create Contact action: Pushing Data to External Apps illustration ## Pushing Related Objects Let's say you want to push a Note along with the Contact and associate it with the contact. For this, you can add another "Create Data Record" node to the flow: Pushing Data to External Apps illustration This flow has two changes compared to the previous version: * Create Note node was added. * Execution path with existing contact found was linked to the new node. Now note will be created regardless of whether we created a contact. To associate Note with the correct contact, you need to set the Contact Id field. This ID can come from two places: `Lookup Contact` or `Create Contact` nodes.\ To account for that, use a "First Not Empty" formula and let it choose the variable to use: Pushing Data to External Apps illustration ## Reacting to Events in Your App Depending on your use case, you may want to push changes (create, update, or delete records) to an external app based on events in your app rather than making explicit API calls to launch actions or flows. To do this, you can: * Create one or more [Internal Events](/docs/managing-membrane/monitoring-troubleshooting/logs/internal-events) that are triggered when changes happen in your app. * Modify the flow(s) to start with an Internal Event trigger instead of (or additionally to) an API trigger. Here is how the resulting flow may look like: Pushing Data to External Apps illustration This flow can be triggered by API or when an event happens in your app. You can implement similar flows for updating or deleting records. ## Extending Functionality After you've pushed data to an external app, you may want to receive updates from it. Check out [Continuous Import](/docs/getting-started/product-integrations/data-integrations/continuous-data-import). ## Live Examples * [Push Companies to CRM](https://examples.getmembrane.com/push-companies-to-a-crm) # In-app AI Agent Source: https://docs.getmembrane.com/docs/getting-started/product-integrations/in-app-ai-agent Give your product's AI agent the ability to interact with your tenants' connected apps. Membrane provides the tools — your agent uses them to read data, create records, send messages, and more across any connected app. ## Getting Started 1. Complete the [Quickstart: Product Integrations](/docs/getting-started/product-integrations) guide. 2. Ask your coding agent to build the agent tooling: > "Give our AI assistant tools from our tenants' connected apps using Membrane. Use static tools loaded at session start." If you want to learn how it works under the hood — read on. ## Approaches ### Static Tools Load a fixed set of tools from the tenant's connections at the start of the session. Best when you know what the agent needs upfront. **When to use:** * The toolset is small and predictable * You want to leverage LLM context caching * Tools don't change during the session **How it works:** 1. List the tenant's [connections](/docs/how-membrane-works/connections). 2. Get available actions for the relevant connections. 3. Pass the actions as tools to your LLM. ```javascript theme={null} // Get tools from a tenant's HubSpot connection const actions = await membrane.connection(connectionId).actions.find() // Convert to LLM tool format const tools = actions.items.map((action) => ({ name: action.key, description: action.name, parameters: action.inputSchema, })) ``` ### Dynamic Tools Let the agent discover and load tools on-demand during a conversation. Best for large tool catalogs or when tools depend on context. **When to use:** * Hundreds or thousands of possible tools * Tools depend on user intent or conversation context * Users may connect new apps mid-session **How it works:** Give your agent two meta-tools: 1. **Search actions** — find relevant tools by intent 2. **Run action** — execute a discovered tool ```javascript theme={null} // Agent searches for relevant tools const results = await membrane.actions.search({ query: 'create a contact in CRM', connectionId: connectionId, }) // Agent runs the selected action const result = await membrane.action(results[0].id).run({ email: 'jane@example.com', firstName: 'Jane', }) ``` The agent dynamically discovers what it can do instead of loading everything upfront. ## End-to-End Example Here's a complete example showing a per-tenant AI agent using the Vercel AI SDK. Each customer gets their own Membrane token, which scopes all operations to their connections. ```typescript theme={null} import { generateText, tool } from 'ai' import { anthropic } from '@ai-sdk/anthropic' import jwt from 'jsonwebtoken' import { MembraneClient } from '@membranehq/sdk' import { z } from 'zod' // 1. Generate a tenant-scoped token (see Authentication docs) function createMembraneClient(tenantId: string, tenantName: string) { const token = jwt.sign( { workspaceKey: process.env.MEMBRANE_WORKSPACE_KEY!, tenantKey: tenantId, name: tenantName }, process.env.MEMBRANE_WORKSPACE_SECRET!, { expiresIn: 7200, algorithm: 'HS512' }, ) // All operations through this client are scoped to the tenant return new MembraneClient({ token }) } // 2. Build tools from tenant's connections async function handleChat(tenantId: string, tenantName: string, userMessage: string) { const membrane = createMembraneClient(tenantId, tenantName) const { text } = await generateText({ model: anthropic('claude-sonnet-4-20250514'), tools: { listDeals: tool({ description: 'List recent deals from the CRM', parameters: z.object({}), execute: async () => { // Runs against this tenant's connected CRM const result = await membrane.action('list-deals').run() return result }, }), createContact: tool({ description: 'Create a new contact in the CRM', parameters: z.object({ email: z.string(), name: z.string(), }), execute: async ({ email, name }) => { const result = await membrane.action('create-contact').run({ email, name }) return result }, }), }, prompt: userMessage, }) return text } ``` This pattern works with any LLM framework (OpenAI SDK, LangChain, etc.) — see the [agent-skills repository](https://github.com/membranehq/agent-skills) for framework-specific adapters. # In-app Workflow Builder Source: https://docs.getmembrane.com/docs/getting-started/product-integrations/in-app-workflow-builder Let your tenants build their own automation workflows between your app and external apps. Your agent can also create and manage workflows programmatically. Membrane provides the triggers, actions, and data schemas — you build the workflow UI and orchestration. ## Getting Started 1. Complete the [Quickstart: Product Integrations](/docs/getting-started/product-integrations) guide. 2. Ask your coding agent to build the workflow builder: > "Build a workflow builder UI where my tenants can create automations between our app and their connected external apps. Use Membrane for triggers and actions." If you want to learn how it works under the hood — read on. ## How It Works 1. **Tenants connect apps** — Using the [Integration Catalog](/docs/getting-started/product-integrations/integration-catalog), tenants connect their accounts. 2. **They build workflows** — Using your workflow builder UI, tenants create automations with triggers and actions. 3. **Membrane provides building blocks** — Event triggers, actions, and data schemas from connected apps are available through the API. 4. **Your app executes workflows** — When triggers fire, your app orchestrates the steps using Membrane actions. ## Key Components ### Event Triggers Workflows start when something happens in an external app. Membrane provides [event subscriptions](/docs/how-membrane-works/events) that notify your app when data changes — via `data-record-created`, `data-record-updated`, and `data-record-deleted` events. **You build:** * UI for selecting an app, data collection, and event type * Backend that creates Membrane flow instances for event subscriptions * Webhook endpoint to receive event notifications **Membrane provides:** * Event subscriptions for connected apps * Data collection metadata (what types of data are available) * Event payloads with record data ### Workflow Actions [Actions](/docs/how-membrane-works/actions) are the steps that do something in external apps — create a contact, send a message, update a record. **You build:** * UI for selecting and configuring actions * Input mapping UI (map data from previous steps) * Execution logic that runs actions in sequence **Membrane provides:** * Pre-built actions for connected apps * Input/output schemas for each action * Action execution with automatic authentication ### Workflow Execution Your app orchestrates execution — receiving trigger events, running actions in sequence, and handling errors. **Common patterns:** * **Synchronous** — Run actions one after another, passing outputs as inputs * **Async with queues** — Use [Temporal](https://temporal.io) or similar for reliable execution * **Parallel** — Run independent actions concurrently ## Example Implementation See the [Workflow Builder Example](https://github.com/membranehq/workflow-builder-example) for a complete reference implementation with Next.js, Temporal, MongoDB, and Membrane React SDK. # Integration Catalog Source: https://docs.getmembrane.com/docs/getting-started/product-integrations/integration-catalog Build an integrations page that lets your tenants connect their apps. We recommend splitting it into two sections: **Connections** (accounts they've already connected) and **Integrations** (apps available to connect). ## Getting Started 1. Complete the [Quickstart: Product Integrations](/docs/getting-started/product-integrations) guide. 2. Ask your coding agent to build the integration catalog for your app: > "Build an integrations page with two sections: 'Your Connections' showing connected accounts with disconnect, and 'Available Integrations' showing apps that can still be connected. Use the Membrane React SDK." If you want to learn how it works under the hood — read on. ## React SDK Install the React SDK and wrap your app with the provider. ```bash theme={null} npm install @membranehq/react ``` ```tsx theme={null} import { MembraneProvider } from '@membranehq/react' export function MyApp() { return ( ) } ``` The `fetchToken` function should call your backend endpoint that generates [authentication tokens](/docs/how-membrane-works/authentication). ### Complete example The key pattern: fetch both integrations and connections, then split integrations into "connected" and "available" based on whether a connection exists. ```tsx theme={null} import { useIntegrations, useConnections, useMembrane } from '@membranehq/react' import { useMemo } from 'react' function IntegrationsPage() { const membrane = useMembrane() const { items: integrations, loading: integrationsLoading } = useIntegrations() const { items: connections, loading: connectionsLoading } = useConnections() // Split integrations into connected vs available const connectedIntegrationIds = useMemo(() => new Set(connections?.map((c) => c.integrationId)), [connections]) const availableIntegrations = useMemo( () => integrations?.filter((i) => !connectedIntegrationIds.has(i.id)), [integrations, connectedIntegrationIds], ) if (integrationsLoading || connectionsLoading) return
Loading...
return (
{/* Section 1: Connected accounts */}

Your Connections

{connections?.length === 0 &&

No connections yet.

} {connections?.map((connection) => (
{connection.name} {connection.disconnected ? ( ) : ( )}
))} {/* Section 2: Available apps to connect */}

Available Integrations

{availableIntegrations?.map((integration) => (
{integration.name}
))}
) } ``` Once an integration is connected, it moves from "Available Integrations" to "Your Connections" automatically. See the [Playground](https://github.com/membranehq/playground) for a full working example with styling and additional features. ### Hooks reference | Hook | Returns | | - | - | | `useIntegrations()` | `{ items: Integration[], loading, error }` — all integrations in your workspace | | `useConnections()` | `{ items: Connection[], loading, error }` — current tenant's connections | | `useMembrane()` | The `MembraneClient` instance for imperative operations | Pass `{ search: query }` to `useIntegrations` to add search filtering. ## JavaScript SDK For non-React apps, use the JavaScript SDK directly. ```bash theme={null} npm install @membranehq/sdk ``` ```javascript theme={null} import { MembraneClient } from '@membranehq/sdk' const membrane = new MembraneClient({ fetchToken: async () => { const response = await fetch('/api/membrane-token') const { token } = await response.json() return token }, }) // List available integrations const integrations = await membrane.integrations.find() // List current tenant's connections const connections = await membrane.connections.find() // Connect to an app (opens popup) await membrane.ui.connect({ integrationKey: 'hubspot' }) // Reconnect a disconnected connection await membrane.ui.connect({ connectionId: 'con_abc123' }) // Disconnect await membrane.connection('con_abc123').archive() ``` ## REST API For any language or framework, use the REST API directly. **List integrations:** ``` GET https://api.getmembrane.com/integrations Authorization: Bearer {TOKEN} ``` **List connections:** ``` GET https://api.getmembrane.com/connections Authorization: Bearer {TOKEN} ``` **Connection UI:** Redirect your user to Membrane's hosted connection page: ``` https://ui.getmembrane.com/embed/integrations/{INTEGRATION_KEY}/connect?token={TOKEN} ``` Optional parameters: `redirectUri` (where to send the user after connecting), `allowMultipleConnections=1`, `name` (pre-set connection name). **Reconnect:** Redirect to: ``` https://ui.getmembrane.com/embed/connections/{CONNECTION_ID}/refresh?token={TOKEN} ``` See the full [API Reference](/reference) for all available endpoints. ## Advanced * [Embedded UI](/docs/ways-to-use-membrane/embedded-ui) — customize the connection flow, support multiple connections per app * [Custom Connection UI](/docs/ways-to-use-membrane/embedded-ui/connection-ui/custom-connection-ui) — build a fully custom connection experience * [Connection Lifecycle](/docs/how-membrane-works/connections) — auto-refresh, reconnection, webhooks # Quickstart: AI Agents Source: https://docs.getmembrane.com/docs/getting-started/quickstart-agent Connect your AI agent to Membrane and give it the ability to interact with any external app — connect accounts, run actions, read data, and subscribe to events. Pick the option that matches your agent: | Your agent | Use | | - | - | | Coding agent with CLI access (Claude Code, Cursor, Windsurf, Codex) | [Agent Skills](#agent-skills) | | MCP-compatible agent (Claude Desktop, ChatGPT, etc.) | [MCP Server](#mcp-server) | | Custom agent or in-app AI assistant | [Custom / In-app Agent](#custom--in-app-agent) | ## Agent Skills If your agent supports skills and you can run commands in the terminal, this is the fastest path. ```bash theme={null} npx skills add membranehq/agent-skills --skill integrate-anything ``` This gives your agent Membrane's integration tools. It sets up the MCP server and credentials automatically. Prompt your agent: > "Connect to HubSpot and create an action that syncs new contacts to our database." See [Agent Skills](/docs/ways-to-use-membrane/agent-skills) for details. ## MCP Server If your agent supports MCP but not skills, add Membrane's hosted MCP server: ```json theme={null} { "mcpServers": { "membrane": { "type": "url", "url": "https://api.getmembrane.com/mcp/integrate-anything" } } } ``` The hosted server handles authentication via OAuth — your agent will prompt you to log in when it first connects. See [MCP Server](/docs/ways-to-use-membrane/mcp) for platform-specific config file locations (Claude Desktop, Cursor, etc.). ## Custom / In-app Agent If you're building your own agent or embedding an AI assistant in your product: * **Custom standalone agent** — see the [agent-skills repository](https://github.com/membranehq/agent-skills) for ready-to-run examples with OpenAI SDK, Vercel AI SDK, LangChain, and OpenCode adapters. * **In-app AI assistant** (multi-tenant, acts on behalf of each user) — see [In-app AI Agent](/docs/getting-started/product-integrations/in-app-ai-agent) for the complete guide with per-user authentication and scoped connections. # Actions Source: https://docs.getmembrane.com/docs/how-membrane-works/actions An action is a single operation your agent or app performs in an external app: create a contact, send a message, list invoices, update a deal. Actions are the primary way to interact with connected apps through Membrane. ## Running actions Running an action means calling **`/act`** — Membrane's unified entry point for operating on an external app through a connection. One endpoint, one shape, multiple surfaces: | Way to Use | How to Run | | - | - | | CLI | `membrane act --connectionKey --api '{"method":"POST","path":"/..."}' --json` | | MCP | Use the `act` tool with one of `api` / `code` / `key` / `id` | | API | [`POST /act`](/reference/workspace-elements/act) with the dispatch in the body | | Console | Action → Run (test with sample input) | Every response is `{ output, actionRunId }`. On failure the body still carries `actionRunId` — feed it to [`GET /action-run-logs/{id}`](/reference/workspace-elements/action-run-logs) for the resolved action, mapped input, errors, and the raw HTTP exchange. ## What goes into `/act` Three dispatch styles, exactly **one** per call. Pick the one that fits the work: ### `api` — inline HTTP request A raw HTTP request sent through the connection's auth and base URL. Default building block for ad-hoc work. ```json theme={null} { "connectionKey": "hubspot-prod", "api": { "method": "POST", "path": "/crm/v3/objects/contacts", "body": { "properties": { "email": "jane@example.com" } } } } ``` ### `code` — sandboxed JavaScript A JS snippet run in a sandbox with an authenticated `membrane` client, `connection`, and `integration` pre-wired. Good for multi-step composites that don't map to a single HTTP call. ```json theme={null} { "connectionKey": "hubspot-prod", "code": "module.exports = async ({ input, membrane }) => { const me = await membrane.proxy.get('/account-info'); return { portalId: me.data.portalId, ...input }; }", "input": { "tag": "qa" } } ``` ### Reusable action A named action Membrane runs on your behalf. Address it by `key` when the action has one (saved actions); use `id` when it doesn't (catalog actions have no `key`). Membrane owns the vendor-specific request shape, auth scoping, and retries. ```json theme={null} { "key": "create-contact", "integrationKey": "hubspot", "connectionKey": "hubspot-prod", "input": { "email": "jane@example.com" } } ``` `api` and `code` always need a connection (via `connectionKey` or `connectionId`). Reusable actions route through the action's own scope, which may bind a specific connection or integration — so the connection field becomes optional when the scope already resolves one. Input is validated against the action's `inputSchema`. Extra properties are silently ignored. The legacy [`POST /actions/{selector}/run`](/reference/workspace-elements/act#legacy-post-actionsselectorrun) endpoint is **deprecated**. It still works for existing callers but new code should use `/act`. ## Reusable actions When you find yourself about to make the same `/act` call a second time, save it as a reusable action. Future calls become `{ key: "" }` instead of the full inline spec, and the action is inspectable in the workspace later. ### Finding Describe what you want to do and Membrane finds matching actions. You can search by intent across your workspace and connected apps. | Way to Use | How to Find | | - | - | | CLI | `membrane action list --connectionId --intent "create a contact"` | | MCP | Use the `list-actions` tool with intent parameter | | API | `GET /actions?connectionId=&intent=create+a+contact` | | SDK | `membrane.actions.find({ connectionId: '', intent: 'create a contact' })` | | Console | Actions page — browse and search | ### Creating Create actions from an intent — describe what the action should do and Membrane configures it automatically. | Way to Use | How to Create | | - | - | | CLI | `membrane action create "create a task" --connectionId ` | | MCP | Use the `create-action` tool | | API | `POST /actions` with `{ "intent": "create a task", "connectionId": "" }` | | SDK | `membrane.actions.create({ intent: 'create a task', connectionId: '' })` | | Console | Actions → Create Action | The action enters a `BUILDING` state while Membrane configures it. Use `--wait` (CLI) or `?wait=true` (API) to wait until ready. ### Updating Update an action's configuration. | Way to Use | How to Update | | - | - | | API | `PATCH /actions/` | | SDK | `membrane.action('').patch({ ... })` | | Console | Action → Edit | ### Deleting Archive an action to remove it. | Way to Use | How to Delete | | - | - | | API | `DELETE /actions/` | | SDK | `membrane.action('').archive()` | | Console | Action → Archive | ## Action Types Actions map to function types that determine what happens when they run: | Type | Description | | - | - | | `api-request-to-external-app` | Make API requests with automatic authentication | | `api-request-to-your-app` | Make an API request to your backend | | `run-javascript` | Run custom JavaScript/TypeScript logic | | `http-request` | Make arbitrary HTTP requests | ## Run logs and replay All action runs are recorded in [Action Run Logs](/reference/workspace-elements/action-run-logs) — input, output, duration, errors, and the raw HTTP exchange. Each log carries an `action` snapshot — the complete `/act` body that was used. POST it back to `/act` to replay the same dispatch: ```bash theme={null} SNAPSHOT=$(curl -s https://api.integration.app/action-run-logs/ \ -H "Authorization: Bearer $TOKEN" | jq '.action') curl -X POST https://api.integration.app/act \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "$SNAPSHOT" ``` Override `input` or `connectionKey` in the snapshot to vary the call: ```bash theme={null} curl -X POST https://api.integration.app/act \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "$(echo "$SNAPSHOT" | jq '.input.email = "updated@example.com"')" ``` # Authentication Source: https://docs.getmembrane.com/docs/how-membrane-works/authentication To use Membrane, you need a Membrane Token. This page explains the different types of tokens and how to get them. ## Membrane Token The main way to authenticate. Sign a JWT with your workspace key and secret. You can find your workspace key and secret on the [Access](https://console.getmembrane.com/w/0/settings/access) page in Console Settings. ```javascript theme={null} import jwt from 'jsonwebtoken' const token = jwt.sign( { workspaceKey: '', tenantKey: '{TENANT_ID}', // Identifier of tenant (user, team, or org) name: '{TENANT_NAME}', // Human-readable name }, '', { expiresIn: 7200, algorithm: 'HS512' }, ) ``` ```python theme={null} import datetime, jwt token = jwt.encode( { "workspaceKey": "", "tenantKey": "{TENANT_ID}", "name": "{TENANT_NAME}", "exp": datetime.datetime.now() + datetime.timedelta(seconds=7200) }, "", algorithm="HS256" ) ``` ```go theme={null} import ( "time" "github.com/golang-jwt/jwt/v5" ) claims := jwt.MapClaims{ "workspaceKey": "", "tenantKey": "{TENANT_ID}", "name": "{TENANT_NAME}", "exp": time.Now().Add(time.Hour * 2).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, _ := token.SignedString([]byte("")) ``` ```ruby theme={null} require 'jwt' payload = { workspaceKey: '', tenantKey: '{TENANT_ID}', name: '{TENANT_NAME}', exp: Time.now.to_i + 7200, } token = JWT.encode(payload, '', 'HS256') ``` ```java theme={null} String token = Jwts.builder() .claim("workspaceKey", "") .claim("tenantKey", "{TENANT_ID}") .claim("name", "{TENANT_NAME}") .setExpiration(Date.from(Instant.now().plusSeconds(7200))) .signWith(new SecretKeySpec("".getBytes(), "HmacSHA256"), SignatureAlgorithm.HS256) .compact(); ``` ```php theme={null} $token = JWT::encode([ 'workspaceKey' => '', 'tenantKey' => '{TENANT_ID}', 'name' => '{TENANT_NAME}', 'exp' => time() + 7200, ], '', 'HS256'); ``` ### Token fields | Field | Required | Description | | - | - | - | | `workspaceKey` | Yes | Your workspace key | | `tenantKey` | No | Identifier of the tenant (user, team, or org). Omit for workspace-level operations. | | `name` | No | Human-readable tenant name (helps with troubleshooting) | | `fields` | No | Additional metadata to store about the tenant | ### Public/Private Key Signing For stronger security, you can sign tokens with a public/private key pair instead of a shared secret. Pass the private key to the sign method and provide the public key in the workspace settings. Use any asymmetric algorithm: `ES256`, `RS256`, `PS256`, etc. ### Test Token For quick testing, get a pre-generated test token from the [Access](https://console.getmembrane.com/w/0/settings/access) page in Console Settings. ### Long-lived Tokens For tokens that don't expire, create a Long-lived Token on the [Access](https://console.getmembrane.com/w/0/settings/access) page in Console Settings under the Client Tokens section. ## Workspace Management Token For administrative operations that affect the entire workspace — managing tenants, updating workspace settings, importing/exporting configuration, and other operations that aren't scoped to a single tenant. Include `isAdmin: true` in the token payload and omit `tenantKey`: ```javascript theme={null} const adminToken = jwt.sign( { workspaceKey: '', isAdmin: true, }, '', { expiresIn: 7200, algorithm: 'HS512' }, ) ``` ## Developer Token Acts on behalf of your Membrane account — can do anything you can across all your workspaces. Get one from API Tokens in [Console Settings](https://console.getmembrane.com/w/0/settings). ## Membrane Instance Token For self-hosted Membrane instances. See [Self-hosting](/docs/managing-membrane/self-hosting) for details. # Binary Data and Files Source: https://docs.getmembrane.com/docs/how-membrane-works/binary-data-and-files The Membrane API works with data in JSON format. When you need to work with binary data (uploading or downloading files), the data is stored in a separate storage and a URI to access it is provided in JSON. ## Files Endpoint For convenience, integration engine provides a special `/files` endpoint that accepts binary files and returns a pre-signed URL to access them. You can use it to: * Download files from external apps: you can use the `/files` endpoint in connector code or your custom code, then return the URL to access them in the output. * Upload files to external apps: you can use the `/files` endpoint in your code and pass the URL to the input of the code running in the engine. ## Expiration Files are stored in the engine storage and are available for a limited time. In our cloud version, the files will be available for 1 hour.\ In self-hosted version, this time may vary based on how your storage is configured and can last up to 7 days. ## Custom Code Example Here is an example of a custom code that can run in the engine and download a large file from an external app: ```javascript theme={null} import axios from 'axios' export default async function myCustomFunction({ externalApiClient, engineApiClient }) { // externalApiClient: makes authenticated requests to the connected app // engineApiClient: makes requests to Membrane's internal API (e.g., file storage) const fileResponse = await externalApiClient.makeApiRequest({ path: '/files/export.csv', method: 'get', responseType: 'stream', // We recommend using streams to work with large files to avoid memory issues returnFullResponse: true, // Return full response to get headers and pass them on to the next request }) const response = await engineApiClient.post('/files', fileResponse.data, { headers: { 'Content-Length': fileResponse.headers['content-length'], 'Content-Type': fileResponse.headers['content-type'], }, // Without this, we load the whole file into memory (which can be several GB). // https://github.com/axios/axios/issues/1045 maxRedirects: 0, }) return { fileUri: response.downloadUri, } } ``` # Connections Source: https://docs.getmembrane.com/docs/how-membrane-works/connections A connection is an authenticated link to an external app. Before you can interact with an app (read data, create records), a connection must be established that authorizes access. See [Connections API Reference](/reference/workspace-elements/connections) for the full API. Membrane handles the full authentication lifecycle: OAuth flows, token refresh, API key storage, reconnection on failure, and credential encryption at rest. ## Connecting Create a connection to an external app. You can connect by intent — just describe what you want to connect to and Membrane finds the right app automatically. | Way to Use | How to Connect | | - | - | | CLI | `membrane connect --intent "CRM"` | | MCP | Use the `create-connection` tool | | API | `POST /connect` with `{ "intent": "CRM" }` | | SDK | `membrane.connect({ intent: 'CRM' })` | | Embedded UI | `membrane.ui.connect({ intent: 'CRM' })` | | Console | Connections → Create Connection | ## Managing Connections ### Listing List all connections for the current tenant. | Way to Use | How to List | | - | - | | CLI | `membrane connection list` | | MCP | Use the `list-connections` tool | | API | `GET /connections` | | SDK | `membrane.connections.find()` | | Console | Connections page | Each connection includes a `disconnected` flag. If `true`, the connection needs re-authentication. ### Reconnecting When a connection is disconnected (expired token, revoked access), Membrane attempts to fix it automatically with token refresh and exponential backoff. To reconnect manually: | Way to Use | How to Reconnect | | - | - | | CLI | `membrane connect --connectionId ` | | API | `POST /connect` with `{ "connectionId": "" }` | | SDK | `membrane.connect({ connectionId: '' })` | | Embedded UI | `membrane.ui.connect({ connectionId: '' })` | | Console | Connection → Reconnect | ### Deleting Archive a connection to disconnect and remove it. | Way to Use | How to Delete | | - | - | | CLI | `membrane connection archive ` | | API | `DELETE /connections/` | | SDK | `membrane.connection('').archive()` | | Console | Connection → Archive | ## Lifecycle Events Subscribe to [webhook notifications](/docs/managing-membrane/monitoring-troubleshooting/webhook-notifications) for connection lifecycle events: * `connection.created` — connection established with credentials * `connection.deleted` — connection archived * `connection.disconnected` — connection lost access * `connection.reconnected` — connection restored # API Proxy Source: https://docs.getmembrane.com/docs/how-membrane-works/connections/api-proxy API proxy lets you make direct API requests through a [Connection](/docs/how-membrane-works/connections).\ API Proxy sends your request to the external application without changes and returns the exact response the external app provided. The API proxy does three things with your requests: * Prepends the base URL to the path you provide (unless you provide a full URL) * Adds authentication headers to the request * Handles credential refreshes automatically ## Using API Proxy ### REST API To make a Proxy request using REST API, simply prefix the request with ```text theme={null} https://api.getmembrane.com/connections//proxy ``` Connection selector can be one of the following: * Integration key (if there is only one connection for the integration) * Integration ID (if there is only one connection for the integration) * Connection ID Let's say you created a connection to the Hubspot CRM and want to make a request to the following hubspot endpoint: ```text theme={null} https://api.hubapi.com/crm/v3/owners/ ``` you would instead make a request to ```text theme={null} https://api.getmembrane.com/connections//proxy/crm/v3/owners/ ``` Or if you don't want to rely on the base URL, you can provide the full URL: ```text theme={null} https://api.getmembrane.com/connections//proxy/https://api.hubapi.com/crm/v3/owners/ ``` The HTTP method and body you use for this request will be proxied to the underlying application's API. ### JavaScript SDK To make a Proxy request using JavaScript SDK, use `proxy` property of the Connection: ```js theme={null} const response = membrane.connection('connection-id').proxy.get('/crm/v3/owners/') ``` You can call `.get`, `.post`, `.patch`, `.put` and `.delete` requests of the `proxy` property.\ First parameter is an API path and the second is an optional object with the request payload for `post`, `patch`, and `put` methods. ## Edge Cases ### Headers There are special headers that are used by the proxy infrastructure. They will be ignored by the proxy and not passed to the underlying application. Here is the full list of these headers: * host * x-request-id * x-real-ip * x-forwarded-for * x-forwarded-host * x-forwarded-port * x-forwarded-proto * x-forwarded-scheme * x-scheme * x-original-forwarded-for * x-different-name * x-request-start * x-using-nginx-controller * x-amzn-trace-id * forwarded-for-header * proxy-protocol * real\_ip\_header * real\_ip\_recursive * cdn-loop * cf-ipcountry * cf-ray * cf-visitor * cf-connecting-ip * connection * authorization * content-length ### Error Handling Proxy will return the same status code, headers, and body that the underlying application's API returns. The only exception is handling credential refreshes. If proxy receives a response indicating that the credentials have expired, it will attempt to refresh the credentials once. Here are the possible outcomes when this happens: * If refresh is successful – it will retry the request and return the second response (successful or not). * If refresh process fails – it will return a Membrane error with details and the log of API requests to external app that led to the error. In case of any error unrelated to the request to external app (i.e. incorrect Membrane access token), a standard Membrane error will be returned. Your code that uses API Proxy should be ready to handle errors in the Membrane format. ## Limits Proxy API has the same limits as the rest of our API endpoints. See [Limits](/docs/managing-membrane/limits). # Events Source: https://docs.getmembrane.com/docs/how-membrane-works/events Events let your agent or app react to changes in external apps — a new contact created in HubSpot, a message posted in Slack, a file uploaded to Google Drive. Membrane subscribes to these changes and delivers them to your application. See [External Events API Reference](/reference/workspace-elements/external-events) for the full API. ## Subscribing to Events Event subscriptions are created through [Flows](/reference/workspace-elements/flows). A flow is a multi-step workflow triggered by an event — in the simplest form, it just sends a webhook to your app. 1. Create a Flow in your Membrane workspace (via Console, CLI, or API). 2. Set the trigger to an event type (see below). 3. Add steps to process the event — the simplest setup is a single `api-request-to-your-app` node that sends the event data to your backend. ## Event Types ### Internal Events Custom events defined by the connector — not tied to a specific data collection. Examples: `form-submitted`, `payment-received`, `message-posted`. Use a `connector-event-trigger` or `app-event-trigger` in your flow. ### Stand-alone External Event Types (experimental) If you want to subscribe to an external event without modifying a connector, you can define an [External Event Type](/reference/workspace-elements/external-events/external-event-types) directly in your workspace. The type owns its own `subscribe` / `handle` / `unsubscribe` JavaScript and can be scoped universally, to a single integration, or to a single connection. Stand-alone External Event Types **do not work as Flow triggers**. To consume their events, create an [External Event Subscription](/reference/workspace-elements/external-events/external-event-subscriptions) with the `connectionId` and `externalEventTypeId`, then `PATCH` a `webhookUri` onto it. Membrane will POST each emitted event to that URI. ### Data Collection Events Triggered by changes to records in a specific data collection (contacts, deals, messages, etc.): * `data-record-created` — a new record appears in the external app * `data-record-updated` — an existing record changes * `data-record-deleted` — a record is removed Use `data-record-created-trigger`, `data-record-updated-trigger`, or `data-record-deleted-trigger` in your flow. #### Polling Intervals For data collection events, Membrane detects changes via webhooks or polling. You can configure the polling interval: * **Pull-based**: default 300 seconds (5 minutes) * **Full scan**: default 10,800 seconds (3 hours) Set these at the trigger node level or data source level. See [Limits](/docs/managing-membrane/limits) for minimum allowed intervals. ## Logging Membrane provides detailed logging for every step of the event pipeline: * **[External Webhooks](/docs/managing-membrane/monitoring-troubleshooting/logs/external-webhooks)** — raw incoming webhook payloads from external apps * **[External Event Pulls](/docs/managing-membrane/monitoring-troubleshooting/logs/external-event-pulls)** — records of each polling cycle * **[External Events](/docs/managing-membrane/monitoring-troubleshooting/logs/external-events)** — processed events with their payload and status * **[Flow Runs](/docs/managing-membrane/monitoring-troubleshooting/logs/flow-runs)** — execution logs for each flow triggered by an event # Internal API Source: https://docs.getmembrane.com/docs/how-membrane-works/internal-api When Membrane makes requests to your API, it will use configuration from the [Internal API settings](https://console.getmembrane.com/w/0/settings/internal-api) in your workspace ## Base URL In the **API client** section of the [Internal API settings](https://console.getmembrane.com/w/0/settings/internal-api), you can configure a base URL (e.g. `https://api.yourdomain.com`) for your Internal API. Once set, the base URL is automatically prepended to relative URIs in your [`api-request-to-your-app`](/docs/references/functions/function-types/api-request-to-your-app) actions. For example, if your base URL is `https://api.yourdomain.com` and your action's `request.uri` is `/notifications`, the final request will be sent to `https://api.yourdomain.com/notifications`. Configuring the base URL once lets you reuse it across all your actions and keeps action definitions portable when migrating workspaces between environments. ## Authentication and API Client You can configure how Membrane will authenticate with your API by choosing an authentication method and providing the necessary configuration. This configuration follows the same structure as [Authentication in Connector Builder](/reference/workspace-elements/connectors/connector-types/auth-proxy) in connectors. ## Credentials Since there is no explicit process of creating a connection between Membrane and your API, you need to provide initial credentials yourself. ### Credentials Schema Credentials Schema is a JSON Schema that describes the structure of credentials you need to authenticate requests to your API. ### Initializing Credentials There are a few ways of initializing the user's credentials. Providing credentials when initializing Membrane Javascript SDK: ```javascript theme={null} import { MembraneClient } from '@membranehq/sdk' const membrane = new MembraneClient({ token: '{TOKEN}', credentials: { apiKey: '{USER_API_KEY}', }, }) ``` Alternatively, you can provide a `fetchCredentials` method that returns credentials asynchronously: ```javascript theme={null} import { MembraneClient } from '@membranehq/sdk' const membrane = new MembraneClient({ token: '{TOKEN}', fetchCredentials: () => fetch('/user-credentials').then((res) => res.json()), }) ```
Whenever you provide `credentials` or `fetchCredentials`, the SDK will associate them with the current user and use them to authenticate requests going forward. You can also update credentials for the current user at any time via SDK or API: ```javascript theme={null} await membrane.self.patch({ credentials: newCredentials }) ```
### Refreshing Credentials If you use OAuth2 authentication type, Membrane will automatically refresh credentials when HTTP 401 response code is received. It will follow the standard OAuth2 token refresh flow: [Refreshing Access Token](https://www.oauth.com/oauth2-servers/access-tokens/refreshing-access-tokens/). # Membrane Universe Source: https://docs.getmembrane.com/docs/how-membrane-works/membrane-universe Membrane Universe is a comprehensive knowledge base about integrations, containing information about external apps, how to connect to them, and pre-built integration logic for common use cases. ## What It Contains Membrane Universe maintains records of: * **External Apps**: Catalog of applications that can be integrated, including their APIs, authentication methods, and capabilities * **Connectors**: Pre-built authentication and API access configurations for connecting to external apps * **Actions**: Ready-to-use actions / tools for popular apps and common use cases. ## AI-Maintained, Human-Curated Membrane Universe is maintained by AI and curated by the Membrane team. This approach ensures: * **Continuous Updates**: New apps and API changes are automatically tracked and incorporated * **Quality Assurance**: Human experts review and validate integration packages * **Community Contributions**: Users can contribute improvements and new integrations ## How It Works When you use Membrane, you automatically benefit from Membrane Universe: * Membrane Agent uses this knowledge base to research external apps and build integrations * Pre-built connectors let you connect to popular apps without writing authentication code * Integration packages provide starting points for common use cases You can browse available integrations in the [Membrane Console](https://console.getmembrane.com) or use the API to query available apps, connectors, and packages. # Tenants Source: https://docs.getmembrane.com/docs/how-membrane-works/tenants Tenants represent the people, teams, organizations, apps, or agents that use integrations in your workspace. They are the primary unit of isolation — each tenant gets their own connections, element customizations, and activity logs. ## How Tenants Work Workspace elements exist at two levels: **Workspace level.** Elements defined at the workspace level serve as shared templates available to all tenants. Tenants can read these but cannot modify them. **Tenant level.** Each tenant can have their own elements and customizations of workspace-level elements — for example, a workspace-level action may have tenant-specific configuration or parameters. Some elements are always workspace-level — for example, integrations are configured once for the whole workspace. Others are always tenant-level — connections, event subscriptions, and activity logs always belong to a specific tenant. Many elements, such as actions, flows, field mappings, data sources, and connectors, can exist at both levels — defined as workspace-wide templates with optional tenant-level customizations. ## Data Visibility Tenants operate under strict data isolation: * **Own data**: Tenants can read and modify their own connections, customizations, and activity logs. * **Workspace data**: Tenants can read workspace-level elements but cannot modify them. * **Membrane Universe**: Tenants can read public connectors, external apps, actions, and other elements from the Membrane Universe but cannot modify them. * **Other tenants**: Tenants can never see or access another tenant's data. This is enforced at the database level. ## Rate Limits You can configure per-tenant rate limits in your workspace settings to control how many resources each tenant can consume (e.g., AI credits per rolling 30-day window). Workspace-level rate limits are shared across all tenants. For full environment isolation (separate configurations, connectors, etc.), use separate workspaces instead. ## Creating Tenants Tenants are created automatically when someone authenticates with a new `tenantKey` in their JWT token (see [Authentication](/docs/how-membrane-works/authentication)). You can also create and manage them via the API — see [Tenants API Reference](/api-reference/customers/create-a-new-customer). ## How Tenant Scoping Works When you generate a JWT token with a tenant's `id` and use it to initialize the SDK or call the API, **all operations are automatically scoped to that tenant**. You don't need to pass a `tenantId` to every API call — the token handles it: ```typescript theme={null} import jwt from 'jsonwebtoken' import { MembraneClient } from '@membranehq/sdk' // Generate a token scoped to a specific tenant const token = jwt.sign( { workspaceKey: process.env.MEMBRANE_WORKSPACE_KEY, tenantKey: 'customer-123', name: 'Acme Corp' }, process.env.MEMBRANE_WORKSPACE_SECRET, { expiresIn: 7200, algorithm: 'HS512' }, ) // Initialize the SDK with this token — all calls are scoped to customer-123 const membrane = new MembraneClient({ token }) // This returns only customer-123's connections const connections = await membrane.connections.find() // This runs against customer-123's connected app const result = await membrane.action('list-deals').run() ``` This is the foundation for building multi-tenant product integrations. Each customer's connections, data, and activity are fully isolated through the token. ## Recommended Uses **One tenant per end-user or organization.** The most common pattern — each customer of your product is a separate tenant with their own OAuth connections, data imports, and integration history. **One tenant per connected app.** If a single user connects multiple instances of the same app (e.g., two Salesforce orgs), you can model each as a separate tenant to keep their connections and data fully isolated. **One tenant per agent.** AI agents that interact with external apps can each be a separate tenant, giving each agent its own set of credentials and activity trail. ## Tenants and Connections Every connection belongs to exactly one tenant. When a tenant authenticates and connects an external app, the resulting connection (with its OAuth tokens or API credentials) is scoped to that tenant alone. If a tenant is archived, all their connections are automatically deactivated. ## Activity Tracking All activity logs are tagged with the tenant ID. Workspace managers can filter logs by tenant in the Console or via the API. Tenants accessing logs through their own token will only see their own activity. # Import and Export Source: https://docs.getmembrane.com/docs/managing-membrane/import-and-export Membrane provides several ways to transfer workspace elements (integrations, actions, flows, field mappings, data sources, data links, and custom connectors) between workspaces. This is commonly used to move configurations from development to production. ## What Gets Transferred **Transferred:** * Integration configurations (which connectors are used) * Actions, flows, and field mappings * Data sources and data links * Custom connector source code * Element structure and relationships **NOT transferred:** * Integration parameters (clientId and secrets, OAuth configurations) * Workspace settings * Internal API definitions * Connection credentials ## Console UI The Console provides a visual interface for importing and exporting workspace elements: 1. Navigate to **Settings > Import/Export** in your workspace 2. **To Export**: Click the export button to download all workspace elements as a zip file 3. **To Import**: Upload the zip file to your target workspace using the import feature This method is ideal for one-time transfers or manual migrations between workspaces. ## API For programmatic or automated transfers, use the Membrane API: **Export workspace elements:** ```bash theme={null} curl -X GET "https://api.getmembrane.com/workspaces/export" \ -H "Authorization: Bearer YOUR_API_KEY" \ --output workspace-export.zip ``` See the [Export Workspace API Reference](/reference/management-api/workspace-migration/export-workspace) for details. **Import workspace elements:** ```bash theme={null} curl -X POST "https://api.getmembrane.com/workspaces/import" \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "file=@workspace-export.zip" ``` See the [Import Workspace API Reference](/reference/management-api/workspace-migration/import-workspace) for details. This method is ideal for CI/CD pipelines, automated deployments, or custom tooling. ## CLI [Membrane CLI](https://www.npmjs.com/package/@membranehq/cli) lets you pull and push workspace elements as YAML files, making it possible to version-control your integration configurations and use standard development workflows. ```shell theme={null} npm install -g @membranehq/cli # Pull workspace elements to local files membrane pull # Push local changes to workspace membrane push ``` See the [Membrane CLI npm page](https://www.npmjs.com/package/@membranehq/cli) for full documentation on installation, configuration, and usage. This method is ideal for local development, version control, and diff tracking. ## Comparison | Method | Best For | Pros | Cons | | - | - | - | - | | **Console UI** | One-time transfers, non-technical users | No setup required, visual interface | Manual process | | **API** | Automation, CI/CD pipelines | Scriptable, integrates with existing tools | Requires API authentication setup | | **CLI** | Local development, version control | Full local workspace sync, diff tracking, IDE integration | Requires CLI installation | # Limits Source: https://docs.getmembrane.com/docs/managing-membrane/limits To ensure fair use of our cloud resources, we have implemented a number of limits. If you use a self-hosted version of Membrane, you can change any of these limits by providing corresponding environment variables. **Note:** All limits are applied on a *rolling window basis*, meaning usage is continuously measured over time rather than resetting at fixed intervals. ## API | | Limit | | - | - | | Maximum request size | 10 Mb | | Maximum response size | 30 Mb | | Maximum request duration | 60 seconds | * If you need to work with larger data, please check [Files](/docs/how-membrane-works/binary-data-and-files). * If you need to run longer requests, use [Flows](/reference/workspace-elements/flows). ## Flow Runs | | Limit | | - | - | | Maximum flow run duration | 50 min | | Maximum memory size | 2 GB | | Maximum flow node output | 20 MB | | Maximum parallel flow runs per connection | 1 | | Maximum flow run queue size per connection | 10,000 | ### Understanding flow run queue limits The **flow run queue size per connection** limit controls how many flow runs can be queued for a single connection before new flow runs are rejected with a rate limit error. **Important architectural consideration:** Simply increasing the queue size limit does not solve throughput issues. If your queue is consistently reaching the limit, it means flow runs are being queued faster than they can be processed. Increasing the queue size will only postpone the problem, not resolve it. **To address high-volume flow processing:** * Increase **parallel flow runs per connection** to process flows faster. * Optimize flow logic to reduce execution time. The queue size limit exists to prevent memory issues and provide early feedback when your system cannot keep up with the incoming load. ## External Event Pulls | | Limit | | - | - | | Maximum parallel incremental pulls per connection | 1 | | Maximum parallel "full sync" pulls per connection | 1 | | Maximum parallel pulls per workspace | 10 | | Minimal interval for incremental pull | 60 seconds | | Minimal interval for "full sync" pull | 60 minutes | ## Action | | Limit | | - | - | | Maximum memory when running custom code | 512 MB | | Maximum duration in synchronous mode | 50 seconds | | Maximum duration in asynchronous mode (flow) | 50 minutes | # Monitoring and Troubleshooting Source: https://docs.getmembrane.com/docs/managing-membrane/monitoring-troubleshooting After you implemented the integrations, you need to make sure they are working as expected. When they don't, you need to understand what is happening and why. ## Logging Membrane keeps and provides you with detailed logs of what is happening in your integrations: * [API Requests](/docs/managing-membrane/monitoring-troubleshooting/logs/api-requests) * [External Webhooks](/docs/managing-membrane/monitoring-troubleshooting/logs/external-webhooks) * [External Event Pulls](/docs/managing-membrane/monitoring-troubleshooting/logs/external-event-pulls) * [External Events](/docs/managing-membrane/monitoring-troubleshooting/logs/external-events) * [Internal Events](/docs/managing-membrane/monitoring-troubleshooting/logs/internal-events) * [Flow Runs](/docs/managing-membrane/monitoring-troubleshooting/logs/flow-runs) ## Webhook Notifications To get notified when an important event happens in your integration workspace, you can subscribe to webhook notifications. Learn more about webhook notifications here: [Webhook Notifications](/docs/managing-membrane/monitoring-troubleshooting/webhook-notifications) ## Troubleshooting There are usually three types of problems with integrations: * Integration ran but resulted in an error. * Integration ran but didn't do what you expected it to do. * Integration did not run when you expected it to. Let's look at how to troubleshoot each of these problems. ### Errors When integration errored, follow red dots to find the reason: Monitoring and Troubleshooting illustration You can always find the error logs on the "Error" tab of the failed run. To understand what led to the error, check out the "Logs" tab. Monitoring and Troubleshooting illustration ### Integrations not doing the right thing When integration ran, but didn't do what you expected it to do, go to the Flow Run page and trace its execution step by step. Monitoring and Troubleshooting illustration Each node run contains detailed logs of what it did and why. You can see the logs by clicking on the node run and selecting the "Logs" tab. ### Integration Not Running When you expected an integration to run, but it did not, check the following: * Check event logs to see if the events you expected did happen: * [Data Source Events](/reference/workspace-elements/data-sources/data-source-instances) for external events. * [Internal Events](/reference/workspace-elements/app-events) for internal events. * If events did happen, check the event details to see if it launched the integration you expected. If not, check the trigger configuration. * If events did not happen, check the following: * For external events: find the [Tenant Data Source](/reference/workspace-elements/data-sources/data-source-instances) you expected to trigger the event and go to the "Subscriptions and Events" tab for troubleshooting. * For internal events: * If you have per-tenant subscriptions, go to the [Tenant Subscriptions](https://console.getmembrane.com/docs/integrations/ux/app-event-types/subscriptions) page, find the subscription you expected to generate the event, and see its details. * If you have global webhooks, try making an API call to the global webhook and check the response. ### Detailed Logs for API requests To see detailed execution logs for any API request you make, you can add `logs=1` to the query parameters of the request. The response will contain a `_logs` property with the detailed logs. Note: this only works when the response is a JSON object. # Logs Source: https://docs.getmembrane.com/docs/managing-membrane/monitoring-troubleshooting/logs Membrane records detailed logs for every step of the integration pipeline. Use these logs to debug issues, monitor health, and understand what's happening in your integrations. | Log Type | What It Records | | - | - | | [API Requests](/docs/managing-membrane/monitoring-troubleshooting/logs/api-requests) | Outgoing HTTP requests to external apps — URL, method, status, duration | | [External Event Pulls](/docs/managing-membrane/monitoring-troubleshooting/logs/external-event-pulls) | Polling cycles for event detection — when it ran, events found, errors | | [External Events](/docs/managing-membrane/monitoring-troubleshooting/logs/external-events) | Processed events with payloads, which flow runs they triggered | | [External Webhooks](/docs/managing-membrane/monitoring-troubleshooting/logs/external-webhooks) | Incoming webhook payloads from external apps — headers, body, response status | | [Flow Runs](/docs/managing-membrane/monitoring-troubleshooting/logs/flow-runs) | Flow execution logs — step-by-step results, duration, errors | | [Internal Events](/docs/managing-membrane/monitoring-troubleshooting/logs/internal-events) | Events sent from your app to Membrane | All logs are available in the Console under [Activity](https://console.getmembrane.com/w/0/activity). # API Requests Source: https://docs.getmembrane.com/docs/managing-membrane/monitoring-troubleshooting/logs/api-requests We record every API request any of your integrations make to external applications. You can find them here: [API Requests](https://console.getmembrane.com/w/0/activity/external-api-logs) # External Event Pulls Source: https://docs.getmembrane.com/docs/managing-membrane/monitoring-troubleshooting/logs/external-event-pulls When an [external event subscription](/reference/workspace-elements/external-events/external-event-subscriptions) uses polling to receive events, we record every pull and all the events it generates. You can find logs of the pulls here: [External Event Pulls](https://console.getmembrane.com/w/0/activity/external-event-pulls) # External Events Source: https://docs.getmembrane.com/docs/managing-membrane/monitoring-troubleshooting/logs/external-events When an event you subscribed to in one of the external apps happens, we record the event and any flows it triggered. You can explore the collected events on the [External Events](https://console.getmembrane.com/w/0/activity/external-events) page. ## See Also * [External Events](/docs/managing-membrane/monitoring-troubleshooting/logs/external-events) * [Flows](/reference/workspace-elements/flows) # External Webhooks Source: https://docs.getmembrane.com/docs/managing-membrane/monitoring-troubleshooting/logs/external-webhooks When integration engine receives an external webhook, we record the payload and any events extracted from it. You can find the logs here: [External Webhooks](https://console.getmembrane.com/w/0/activity/external-webhook-logs) # Flow Runs Source: https://docs.getmembrane.com/docs/managing-membrane/monitoring-troubleshooting/logs/flow-runs Flow Runs illustration Whenever a [Flow](/reference/workspace-elements/flows) runs, we record the following: * What launched the flow (an event, schedule, or an API call). * Flow inputs. * Runs of every node in the flow. * Logs of every run: * Data transformations. * API requests. You can see the list of runs on the [Flow Runs](https://console.getmembrane.com/activity/flow-runs) page. # Internal Events Source: https://docs.getmembrane.com/docs/managing-membrane/monitoring-troubleshooting/logs/internal-events When your app sends an event to the integration engine, we record the event and any flows it triggered. You can explore the collected events here: [Internal Events](https://console.getmembrane.com/w/0/activity/app-events) ## See Also * [Internal Events](/docs/managing-membrane/monitoring-troubleshooting/logs/internal-events) * [Flows](/reference/workspace-elements/flows) # Webhook Notifications Source: https://docs.getmembrane.com/docs/managing-membrane/monitoring-troubleshooting/webhook-notifications Webhook notifications let your backend react to events in your Membrane workspace — connections being created, disconnected, or archived. ## Setup Configure a webhook URL in the Console under [Settings > Webhook Notifications](https://console.getmembrane.com/w/0/settings/webhook-notifications). Membrane sends a POST request to your URL whenever a matching event occurs. ## Event Types ### Connection Events | Event | When it fires | | - | - | | `connection.created` | A new connection is established (OAuth completed, API key saved) | | `connection.disconnected` | A connection lost access (token expired, credentials revoked) | | `connection.reconnected` | A previously disconnected connection is restored | | `connection.deleted` | A connection is archived | ### Flow Run Events | Event | When it fires | | - | - | | `flowRun.queued` | A flow run is queued for execution | | `flowRun.started` | A flow run begins executing | | `flowRun.completed` | A flow run finishes successfully | | `flowRun.failed` | A flow run fails with an error | | `flowRun.stopped` | A flow run is manually stopped | ### Alert Events | Event | When it fires | | - | - | | `alert.created` | An alert is created or becomes ongoing | ## Payloads Each webhook POST body contains an `eventType` field and a `data` object. The shape of `data` depends on the event category. ### Connection events ```json theme={null} { "eventType": "connection.created", "data": { "connection": { "id": "con_abc123", "integrationId": "int_xyz789", "integrationKey": "slack", "name": "Slack", "disconnected": false, "customerId": "customer-123" } } } ``` ### Flow run events ```json theme={null} { "eventType": "flowRun.completed", "data": { "flowRun": { "id": "fr_abc123", "connectionFlowId": "cf_xyz789", "integrationFlowId": "if_abc456", "state": "completed", "startTime": "2024-01-15T10:30:00.000Z", "endTime": "2024-01-15T10:30:05.123Z", "startNodeKey": "trigger", "launchedBy": { "type": "app-event-trigger" } } } } ``` The `flowRun.failed` event includes an `errors` array on the flow run object with details about what went wrong. ### Alert events ```json theme={null} { "eventType": "alert.created", "data": { "alert": { "id": "alrt_abc123", "type": "connection_disconnected", "severity": "warning", "description": "Slack connection lost access", "status": "ongoing", "workspaceId": "ws_xyz789", "workspaceUrl": "https://console.getmembrane.com/w/org-workspace-id", "createdAt": "2024-01-15T10:30:00.000Z" } } } ``` ## Handling Webhooks Example Express.js endpoint: ```typescript theme={null} app.post('/api/membrane-webhook', async (req, res) => { const { eventType, data } = req.body switch (eventType) { case 'connection.created': // Run an action on the new connection const membrane = new MembraneClient({ token: process.env.MEMBRANE_ACCESS_TOKEN }) await membrane .action('send-slack-message') .run({ channel: 'general', text: 'Your app is now connected!' }, { connectionId: data.connection.id }) break case 'connection.disconnected': // Notify user to reconnect break case 'flowRun.failed': // Log or alert on flow run failure console.error('Flow run failed', data.flowRun.errors) break case 'alert.created': // Forward alert to your monitoring system break } res.sendStatus(200) }) ``` ## Retry Behavior If your endpoint returns a non-2xx status code, Membrane retries with exponential backoff. ## See Also * [Connections](/docs/how-membrane-works/connections) — connection lifecycle and management * [Events](/docs/how-membrane-works/events) — data events from external apps (distinct from webhook notifications) # Security and Privacy Source: https://docs.getmembrane.com/docs/managing-membrane/security-and-privacy Membrane engine is designed to minimize security and privacy risks. We apply a number of measures to achieve that. ## Certifications and Compliance Membrane is SOC 2 Type 2 certified. Our servers and data are fully located in the European Union and we are GDPR-compliant. For SOC 2 Type 2 reports and Penetration test results, Visit our [trust page](https://trust.getmembrane.com/) ## Encrypted credentials All tenant credentials are stored in encrypted form in the database.\ They are only decrypted before making API requests. Decrypted versions are not stored anywhere outside the system memory. We take measures to exclude credentials from any logs or error reports. ## No permanent user data storage Any user data flowing through the system is stored for 14 days on AWS S3 for logging and debugging purposes. It is then automatically and permanently erased by AWS S3 lifecycle rules. This means no user data is stored beyond the last 14 days, which excludes you from the need to handle GDPR and similar requests for data erasure. ## No third parties We do not pass user data to any third parties except the AWS S3 service and Cloudflare. ## Running on your own infrastructure If the measures above are not sufficient for your security and privacy requirements, you can run Membrane on your own infrastructure and take full control of the data. Please contact [support@getmembrane.com](mailto:support@getmembrane.com) to explore this option. # Self Hosting Source: https://docs.getmembrane.com/docs/managing-membrane/self-hosting Our product is distributed as a set of Docker images that can be deployed in your infrastructure. ## Docker Registry Access Contact our support team to receive access credentials to our Docker registry (`harbor.getmembrane.com`). You'll receive a username in the format `robot$` and a password. ```bash theme={null} docker login harbor.getmembrane.com ``` ### Image Versioning Images are tagged with `:latest` and date-based immutable tags (e.g., `2026-01-14`). For production deployments, we recommend using the immutable tags: `harbor.getmembrane.com/core/api:2026-01-14`. ## Infrastructure Requirements Membrane requires: * Cloud storage (AWS S3, Azure Blob Storage, or Google Cloud Storage) * MongoDB server * Redis server * Authentication provider: Auth0 (free tier is sufficient) or BetterAuth ## Authentication Membrane self-hosting supports two authentication providers: * **Auth0** * **BetterAuth** Use the same provider in both API and Console services. ### Choose Your Setup * **Auth0**: use Auth0-hosted authentication for users. * **BetterAuth + Resend**: use BetterAuth with Resend-managed email delivery. * **BetterAuth + Webhook**: use BetterAuth with your own email delivery backend. * **BetterAuth + OIDC**: use BetterAuth with enterprise SSO via OpenID Connect. ### Auth0 #### Auth0 Application Setup When configuring your Auth0 application: * Application Type: Single-page Application * Allowed Callback URLs: Base URL of your console service * Allowed Web Origins: Base URL of your console service #### API Pods Environment Variables (Auth0) Configure these variables on all API pods. | Variable | Required | Description | | :- | :- | :- | | `AUTH_PROVIDER` | Yes | Set to `auth0` to force Auth0 mode | | `AUTH0_DOMAIN` | Yes | Auth0 tenant domain | | `AUTH0_CLIENT_ID` | Yes | Auth0 application client ID | | `AUTH0_CLIENT_SECRET` | Yes | Auth0 application secret | #### Console Pods Environment Variables (Auth0) | Variable | Required | Description | | :- | :- | :- | | `NEXT_PUBLIC_AUTH_PROVIDER` | Yes | Set to `auth0` | | `NEXT_PUBLIC_AUTH0_DOMAIN` | Yes | Auth0 tenant domain for console login | | `NEXT_PUBLIC_AUTH0_CLIENT_ID` | Yes | Auth0 client ID for console login | ### BetterAuth #### API Pods Environment Variables (BetterAuth) Configure these variables on all API pods. | Variable | Required | Description | | :- | :- | :- | | `AUTH_PROVIDER` | Yes | Set to `betterauth` to force BetterAuth mode | | `AUTH_SECRET` | Yes | BetterAuth session signing secret | | `CONSOLE_BASE_URI` | Yes | Console URL used for auth redirects and verification flows | | `BETTERAUTH_EMAIL_PROVIDER` | Conditional | Required unless `AUTH_OIDC_ENABLED=true`; allowed values: `webhook`, `resend` | #### BetterAuth Email Providers Choose one email provider for BetterAuth email delivery. ##### Resend (API pod environment variables) Set these variables on all API pods: | Variable | Required | Description | | :- | :- | :- | | `BETTERAUTH_EMAIL_PROVIDER` | Yes | Set to `resend` | | `RESEND_API_KEY` | Yes | Resend API key | | `BETTERAUTH_EMAIL_FROM` | Yes | Sender email address used by BetterAuth emails | ##### Webhook (database-backed configuration) Set this variable on all API pods: | Variable | Required | Description | | :- | :- | :- | | `BETTERAUTH_EMAIL_PROVIDER` | Yes | Set to `webhook` | ###### Setup Database Documents When using `webhook`, create both webhook types in the `webhooks` collection: * `better-auth-signup-verify` (for email verification messages) * `better-auth-reset-password` (for password reset messages) ```javascript theme={null} // Open your DB CLI // Select database use // Insert BetterAuth webhook docs db["webhooks"].insertMany([ { type: "better-auth-signup-verify", url: "", secret: "", createdAt: new Date(), updatedAt: new Date() }, { type: "better-auth-reset-password", url: "", secret: "", createdAt: new Date(), updatedAt: new Date() } ]) ``` ###### Document Fields Values to include in each webhook document: * `type`: one of `better-auth-signup-verify` or `better-auth-reset-password`. * `url`: your endpoint that sends the email. * `secret`: optional but strongly recommended; if set, Membrane sends `X-Signature` (HMAC-SHA256). * `createdAt` and `updatedAt`: timestamps for record management. See [System Webhooks](/docs/managing-membrane/self-hosting/system-webhooks) for signature verification and webhook handler guidance. #### Social Logins ##### API pod environment variables (Social Logins) | Variable | Required | Description | | :- | :- | :- | | `AUTH_GOOGLE_CLIENT_ID` | Conditional | Required to enable authentication using Google | | `AUTH_GOOGLE_CLIENT_SECRET` | Conditional | Required to enable authentication using Google | | `AUTH_GITHUB_CLIENT_ID` | Conditional | Required to enable authentication using GitHub | | `AUTH_GITHUB_CLIENT_SECRET` | Conditional | Required to enable authentication using GitHub | | `AUTH_MICROSOFT_CLIENT_ID` | Conditional | Required to enable authentication using Microsoft | | `AUTH_MICROSOFT_CLIENT_SECRET` | Conditional | Required to enable authentication using Microsoft | ##### Console pod environment variables (Social Logins) | Variable | Required | Description | | :- | :- | :- | | `NEXT_PUBLIC_BETTERAUTH_SOCIAL_PROVIDERS` | Conditional | Comma-separated providers shown in login UI (`google,github,microsoft`) | #### OpenID Connect (OIDC) Use either discovery mode (`AUTH_OIDC_DISCOVERY_URL`) or manual endpoint mode (`AUTH_OIDC_AUTHORIZATION_URL` + `AUTH_OIDC_TOKEN_URL`). ##### API pod environment variables (OIDC) | Variable | Required | Description | | :- | :- | :- | | `AUTH_OIDC_ENABLED` | Yes | Set to `true` to enable custom OIDC | | `AUTH_OIDC_CLIENT_ID` | Yes | OIDC client ID | | `AUTH_OIDC_CLIENT_SECRET` | Yes | OIDC client secret | | `AUTH_OIDC_DISCOVERY_URL` | Conditional | Use this for discovery mode | | `AUTH_OIDC_AUTHORIZATION_URL` | Conditional | Required with `AUTH_OIDC_TOKEN_URL` when not using discovery | | `AUTH_OIDC_TOKEN_URL` | Conditional | Required with `AUTH_OIDC_AUTHORIZATION_URL` when not using discovery | | `AUTH_OIDC_PROVIDER_ID` | No | Provider id, default `oidc` | | `AUTH_OIDC_SCOPES` | No | Comma-separated scopes, default `openid,email,profile` | | `AUTH_OIDC_PKCE` | No | Enable PKCE for authorization code flow | | `AUTH_OIDC_REQUIRE_ISSUER_VALIDATION` | No | Enforce strict issuer validation | ##### Console pod environment variables (OIDC) | Variable | Required | Description | | :- | :- | :- | | `NEXT_PUBLIC_BETTERAUTH_OIDC_ENABLED` | Conditional | Set to `true` to show OIDC login button | | `NEXT_PUBLIC_BETTERAUTH_OIDC_PROVIDER_ID` | No | Must match API `AUTH_OIDC_PROVIDER_ID` (default: `oidc`) | | `NEXT_PUBLIC_BETTERAUTH_OIDC_BUTTON_LABEL` | No | Custom login button label | ## Core Services Membrane consists of four essential services: ### API Service The primary engine API that stores and executes integrations. **Docker Image:** `harbor.getmembrane.com/core/api` The API service operates in four distinct modes, each activated by specific environment variables:
#### 1. API Mode Main backend service handling incoming traffic and processing HTTP requests. **Mode-specific Environment Variables:** | Variable | Example | Description | | :- | :- | :- | | `IS_API` | 1 | Enables API mode | | `HEADERS_TIMEOUT_MS` | 61000 | Maximum time to receive request headers | | `KEEPALIVE_TIMEOUT_MS` | 61000 | Maximum time to keep idle connections alive |
#### 2. Instant Tasks Worker Mode Designed for executing semi-instant asynchronous tasks. This mode should be scaled to prevent task queuing. Each worker processes one background job at a time. **Mode-specific Environment Variables:** | Variable | Example | Description | | :- | :- | :- | | `IS_INSTANT_TASKS_WORKER` | 1 | Enables instant tasks worker mode |
#### 3. Queued Tasks Worker Mode Handles long-running tasks such as flow runs, event pulls, and external events. Each worker processes one background job at a time. When limits are enabled, tasks are queued and executed to ensure fair resource distribution among tenants. **Mode-specific Environment Variables:** | Variable | Example | Description | | :- | :- | :- | | `IS_QUEUED_TASKS_WORKER` | 1 | Enables queued tasks worker mode | | `MAX_QUEUED_TASKS_MEMORY_MB` | 1024 | Memory limit for task execution (default: `1024`) | | `MAX_QUEUED_TASKS_PROCESS_TIME_SECONDS` | 3000 | Time limit for task execution (default: `3000`) |
#### 4. Orchestrator Mode Manages schedule triggers, handles data source syncs, and performs cleanup tasks. **Mode-specific Environment Variables:** | Variable | Example | Description | | :- | :- | :- | | `IS_ORCHESTRATOR` | 1 | Enables orchestrator mode | All services scale horizontally without additional configuration (aside from load balancing for API services).
**Common Environment Variables** The following variables are required for all operation modes:
Variable Description Example
`NODE_ENV` Environment mode production
`BASE_URI` Service deployment URL [https://api.yourdomain.com](https://api.yourdomain.com)
`BASE_WEBHOOKS_URI` Optional: Override webhook endpoint base URL. Use this when you need webhooks to be accessible at a different URL than the main API (e.g., when the API is private but webhooks need a public proxy). Works the same way as BASE\_URI, but only for webhook endpoints. [https://webhooks-proxy.yourdomain.com](https://webhooks-proxy.yourdomain.com)
`BASE_OAUTH_CALLBACK_URI` Optional: Override OAuth callback endpoint base URL. Use this when you need OAuth callbacks to be accessible at a different URL than the main API (e.g., when the API is private but OAuth callbacks need a public proxy). Works the same way as BASE\_URI, but only for OAuth callback endpoints. [https://oauth-proxy.yourdomain.com](https://oauth-proxy.yourdomain.com)
`BASE_URI_INTERNAL` Internal service deployment URI (to be used for call from workers to the service) [https://api.yourdomain.com](https://api.yourdomain.com)
`CUSTOM_CODE_RUNNER_URI` Custom Code Runner service URL [https://custom-code-runner.yourdomain.com](https://custom-code-runner.yourdomain.com)
Authentication variables Provider-specific API authentication variables. See Authentication section. `AUTH_PROVIDER`, `AUTH0_*`, `AUTH_SECRET`, `BETTERAUTH_*`
`TMP_STORAGE_BUCKET` Temporary storage bucket (recommend auto-expiration policy) integration-app-tmp
`CONNECTORS_STORAGE_BUCKET` Connectors storage bucket integration-app-connectors
`STATIC_STORAGE_BUCKET` Static files storage bucket integration-app-static
`BASE_STATIC_URI` Static content base URL (files uploaded to the static bucket should be served from this URL) [https://static.yourdomain.com](https://static.yourdomain.com)
`REDIS_URI` or `REDIS_CLUSTER_URI_X` URL for Redis server. For Redis cluster, provide multiple: REDIS\_CLUSTER\_URI\_1, REDIS\_CLUSTER\_URI\_2, ... redis\://user:[password@redis-service.com](mailto:password@redis-service.com):6379/
`WORKER_REDIS_URI` or `WORKER_REDIS_CLUSTER_URI_X` Optional. URL for a dedicated Redis server for background worker tasks. This allows you to separate worker task execution from main Redis operations for better performance and isolation. If not provided, the main Redis server will be used for both purposes. For Redis clusters, provide multiple: WORKER\_REDIS\_CLUSTER\_URI\_1, WORKER\_REDIS\_CLUSTER\_URI\_2, etc. redis\://user:[password@redis-service.com](mailto:password@redis-service.com):6379/
`SECRET` JWT token signing secret s3cr3tString
`ENCRYPTION_SECRET` Credentials encryption secret v3rys3cr3tstring
`MONGO_URI` MongoDB connection string mongodb+srv://login:[pass@mymongo.com](mailto:pass@mymongo.com)/integration-api
`PORT` Container listening port (default: 5000) 5000
`AWS_REGION` AWS S3 region (when using S3 storage) eu-central-1
`AWS_ACCESS_KEY_ID` AWS S3 access key (when using S3 storage)
`AWS_SECRET_ACCESS_KEY` AWS S3 secret key (when using S3 storage)
`STORAGE_PROVIDER` Storage provider type: `s3` (AWS S3), `abs` (Azure Blob Storage), or `gcs` (Google Cloud Storage). Default: `s3` abs
`AZURE_STORAGE_CONNECTION_STRING` Azure Blob Storage connection string (when using Azure storage)
`AZURE_STORAGE_ACCOUNT_NAME` Azure storage account name (alternative to connection string)
`AZURE_STORAGE_ACCOUNT_KEY` Azure storage account key (alternative to connection string)
`GOOGLE_CLOUD_PROJECT_ID` Google Cloud project ID (when using GCS storage)
`GOOGLE_CLOUD_KEYFILE` Path to Google Cloud service account key file (optional, uses Application Default Credentials if not provided)
`ENABLE_LIMITS` Optional: Enable workspace resource limits true
`CONNECTION_CREDENTIALS_STORAGE_TYPE` Optional: storage type for connection credentials: `database`, `external_api`. Default: `database`
`CONNECTION_CREDENTIALS_EXTERNAL_API_ENDPOINT_URL` When `external_api` chosen for connection credentials storage, this is required. [https://api.yourdomain.com](https://api.yourdomain.com)
`MAX_NODE_RUN_OUTPUT_SIZE_MB` Optional: Maximum size in megabytes for node run outputs. Default: `20` 50
`MONGO_MAX_IDLE_TIME_MS` Maximum time that a connection can remain idle in the pool before being closed. Default: 0 (connections never timeout) 300000
`MONGO_MIN_POOL_SIZE` Minimum number of connections to maintain in the MongoDB connection pool. Default: 0 (no minimum) 3
`MONGO_MAX_POOL_SIZE` Maximum number of connections allowed in the MongoDB connection pool. Default: 100 100
`MONGO_SERVER_SELECTION_TIMEOUT_MS` Maximum time in milliseconds to wait for MongoDB server selection. Useful in containerized environments with slower DNS resolution. Default: 30000 (MongoDB default) 60000
`REDIS_CONNECT_TIMEOUT_MS` Maximum time in milliseconds to wait for Redis connection. Default: 10000 (standalone), 60000 (cluster) 60000
`REDIS_DISABLE_TLS_VERIFICATION` Set to `true` to disable TLS certificate verification for Redis connections. Required for AWS ElastiCache with in-transit encryption. true
`SKIP_HEALTH_CHECKS` Skip specific health checks during startup. Use `all` to skip all checks, or a comma-separated list of: `mongo`, `redis`, `storage`, `custom_code`. Useful for Kubernetes environments with slower initial DNS resolution. all
`HEALTH_CHECK_RETRIES` Number of retry attempts for health checks after initial failure. Health checks use exponential backoff with jitter between retries. Default: 3 5
`HEALTH_CHECK_RETRY_DELAY_MS` Initial delay between health check retries in milliseconds. Subsequent retries use exponential backoff (delay doubles each retry). Default: 1000 2000
`HEALTH_CHECK_MAX_RETRY_DELAY_MS` Maximum delay between health check retries in milliseconds. Prevents exponential backoff from growing too large. Default: 10000 30000
`SELF_HOSTING_TOKEN` **Required for self-hosted deployments.** Token for validating your self-hosted Membrane installation. Generate this token from your organization settings in console. The token is validated against Membrane's cloud API on startup. sht\_abc123...
### UI Service Provides pre-built integration user interfaces. **Docker Image:** `harbor.getmembrane.com/core/ui` **Environment Variables:** | Variable | Description | Example | | - | - | - | | `NEXT_PUBLIC_ENGINE_URI` | API service URL | [https://api.yourdomain.com](https://api.yourdomain.com) | | `BASE_PATH` | Optional: URL path prefix for subpath deployments | /ui-app | | `PORT` | Container listening port | 5000 | ### Console Service Administration interface for managing integrations. **Docker Image:** `harbor.getmembrane.com/core/console` **Environment Variables:** | Variable | Description | Example | | - | - | - | | `NEXT_PUBLIC_BASE_URI` | Console access URL | [https://console.integration.yourdomain.com](https://console.integration.yourdomain.com) | | `NEXT_PUBLIC_ENGINE_API_URI` | API service URL | [https://api.integrations.yourdomain.com](https://api.integrations.yourdomain.com) | | `NEXT_PUBLIC_ENGINE_UI_URI` | UI service URL | [https://ui.integrations.yourdomain.com](https://ui.integrations.yourdomain.com) | | Authentication variables | Provider-specific console authentication variables. See [Authentication](#authentication) section. | `NEXT_PUBLIC_AUTH_PROVIDER`, `NEXT_PUBLIC_AUTH0_*`, `NEXT_PUBLIC_BETTERAUTH_*` | | `BASE_PATH` | Optional: URL path prefix for subpath deployments | /console-app | | `PORT` | Container listening port | 5000 | | `NEXT_PUBLIC_ENABLE_LIMITS` | Optional: Enable limits management UI | true | #### Subpath Deployments The `BASE_PATH` environment variable enables deployment on a URL subpath (e.g., `integrations.yourdomain.com/ui-app` or `integrations.yourdomain.com/console-app`) rather than requiring a dedicated domain or subdomain. When set, it automatically adjusts all asset paths to work correctly under the specified path prefix. **Example:** Setting `BASE_PATH=/console-app` allows the Console service to be accessed at `https://integrations.yourdomain.com/console-app` with all resources loading from the correct paths. ### Custom Code Runner Provides an isolated environment for executing custom code in connectors or middleware. This service should only be accessible internally from other services. **Docker Image:** `harbor.getmembrane.com/core/custom-code-runner` > **Note**: On AMD architecture (not ARM), set `CUSTOM_CODE_MEMORY_LIMIT` environment variable for the API service to at least 21474836480 (20GB) to ensure sufficient virtual memory for WebAssembly. Physical memory allocation of 2GB is typically sufficient. ## Scaling Recommendations Backend services emit custom metrics that help determine scaling conditions. The following table outlines these metrics: | Metric | Emitted by | Endpoint | Description | | :- | :- | :- | :- | | `instant_tasks_worker_utilization_rate` | `instant-tasks-worker` | `/prometheus/instant-tasks` | Value (0.0-1.0) representing the percentage of time instant tasks worker is actively processing tasks | | `custom_code_runner_total_job_spaces` | `custom-code-runner` | `/api/v2/prometheus` | Total number of job spaces supported by a custom-code-runner pod | | `custom_code_runner_remaining_job_spaces` | `custom-code-runner` | `/api/v2/prometheus` | Available number of job spaces in a custom-code-runner pod | | `queued_tasks_worker_utilization_rate` | `queued-tasks-worker` | `/prometheus/queued-tasks` | Value (0.0-1.0) representing the percentage of time queued tasks worker is actively processing tasks. | All services scale horizontally. The following table outlines the scaling recommendations for each service for production workloads:
Container Scaling Approach Recommended Values
Console Fixed number of instances to ensure availability and zero-downtime updates. Instances: 2
UI Fixed number of instances to ensure availability and zero-downtime updates. Instances: 2
Orchestrator Fixed number of instances to ensure availability and zero-downtime updates. Instances: 2
API Dynamic scaling based on resource usage. Monitor memory and CPU usage to determine scaling needs. Threshold: 50% Min instances: 2
Instant Tasks Worker Dynamic scaling based on the number of active and waiting tasks, using a modifier to adjust the scaling sensitivity. Pseudocode:`avg(instant_tasks_worker_utilization_rate)` Threshold: 0.75 Min instances: 2
Queued Tasks Worker Dynamic scaling based on workers utilization rate. Pseudocode:`avg(queued_tasks_worker_utilization_rate)` Threshold: 0.85 Min instances: 2
Custom Code Runner Dynamic scaling based on job spaces availability. Monitor the number of total and remaining job spaces and auto scale maintain a certain rate. Pseudocode: `(custom_code_runner_total_job_spaces -custom_code_runner_remaining_job_spaces ) / custom_code_runner_total_job_spaces` Threshold: 0.45 Min instances: 2

## Connector Management ### Automated Deployment Use [Membrane CLI](https://www.npmjs.com/package/@membranehq/membrane-cli) to migrate connectors from cloud to self-hosted environments. ### Manual Deployment Upload connector .zip archives through the Console UI via Integrations > Apps > Upload Connector. ## Troubleshooting For enhanced debugging output, add `DEBUG_ALL=1` to any container's environment variables. ## Helm Guide for installing Membrane using Helm chart. ## Cloud-specific Guides Guide for deploying Membrane on AWS. Guide for deploying Membrane on Azure. Guide for deploying Membrane on Google Cloud Platform. ## Resource Limiting To enable resource limits by workspace and tenant: 1. Add `ENABLE_LIMITS=1` to the API container 2. Add `NEXT_PUBLIC_ENABLE_LIMITS=1` to the Console container This enables workspace managers to set tenant-level limits and platform administrators to configure workspace-level resource restrictions. ## FAQ and Advanced Configuration ### Self-Hosting Limitations * **Managed OAuth Credentials**: Auth-proxy (managed credentials) is not supported. Membrane provides managed OAuth credentials for some apps in the cloud version, but this feature is not available in self-hosted deployments. * **Pre-built Connectors**: The full library of pre-built connectors is not automatically available. You need to [manually migrate connectors](https://community.getmembrane.com/t/how-to-access-pre-built-connectors-in-a-self-hosted-deployment/71) from cloud to self-hosted environments using the Membrane CLI. ### Resource Requirements * **Minimum Requirements**: 500 millicores (0.5 CPU) and 2GB of memory per container is sufficient for most deployments. ### Data Persistence and Backups * **MongoDB**: Requires regular backups * **S3 Storage**: All buckets should be backed up regularly * **Redis**: Used only as a cache; can be safely rebooted or erased ### Health Monitoring * **HTTP Health Checks**: The root endpoint of each service (e.g., `https://api.yourdomain.com/`) serves as a health check endpoint * **Worker Health Checks**: Workers and custom code runners also expose an HTTP server at their root endpoint for health monitoring ### Security * We monitor containers daily with the following SLAs: * Critical issues: 1 business day * High severity issues: 3 business days * Other issues: 2 weeks ### Logging and Error Handling * **Log Format**: Services log to stdout/stderr in plain text # AWS Source: https://docs.getmembrane.com/docs/managing-membrane/self-hosting/aws # Self-hosting on AWS This guide provides AWS-specific information for deploying Membrane in your AWS environment. ## S3 Storage configuration ### Create the necessary S3 buckets: ```Text s3.tf theme={null} resource "aws_s3_bucket" "tmp" { bucket = "${var.environment}-integration-app-tmp" } resource "aws_s3_bucket" "connectors" { bucket = "${var.environment}-integration-app-connectors" } resource "aws_s3_bucket" "static" { bucket = "${var.environment}-integration-app-static" } # Lifecycle rules for tmp bucket resource "aws_s3_bucket_lifecycle_configuration" "tmp" { bucket = aws_s3_bucket.tmp.id rule { id = "cleanup" status = "Enabled" filter { prefix = "" } expiration { days = 7 } } } # CORS configuration for static bucket resource "aws_s3_bucket_cors_configuration" "static" { bucket = aws_s3_bucket.static.id cors_rule { allowed_headers = ["*"] allowed_methods = ["GET"] allowed_origins = ["*"] max_age_seconds = 3000 } } ``` ### Create CloudFront Distribution: ```Text cloudFront.tf theme={null} resource "aws_cloudfront_origin_access_control" "static" { name = "${var.environment}-static-oac" description = "OAC for static S3 bucket" origin_access_control_origin_type = "s3" signing_behavior = "always" signing_protocol = "sigv4" } resource "aws_cloudfront_distribution" "static" { enabled = true default_root_object = "index.html" aliases = ["static.${var.environment}.${var.hosted_zone_name}"] origin { domain_name = aws_s3_bucket.static.bucket_regional_domain_name origin_id = aws_s3_bucket.static.id origin_access_control_id = aws_cloudfront_origin_access_control.static.id } default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] target_origin_id = aws_s3_bucket.static.id viewer_protocol_policy = "redirect-to-https" forwarded_values { query_string = false cookies { forward = "none" } } } price_class = "PriceClass_100" restrictions { geo_restriction { restriction_type = "none" } } viewer_certificate { acm_certificate_arn = aws_acm_certificate.cloudfront.arn ssl_support_method = "sni-only" minimum_protocol_version = "TLSv1.2_2021" } tags = { Service = "core" } } resource "aws_s3_bucket_policy" "static_cloudfront" { bucket = aws_s3_bucket.static.id policy = jsonencode({ Version = "2012-10-17", Statement = [ { Effect = "Allow", Principal = { Service = "cloudfront.amazonaws.com" }, Action = "s3:GetObject", Resource = "${aws_s3_bucket.static.arn}/*", Condition = { StringEquals = { "AWS:SourceArn" = aws_cloudfront_distribution.static.arn } } } ] }) } ``` ## IAM Role Configuration Membrane containers support AWS IAM role-based access to S3 and other AWS services. This is the preferred method over providing explicit access and secret keys. ### Container IAM Configuration To use IAM roles instead of access keys: 1. Create an IAM role with appropriate S3 permissions 2. Assign this role to your ECS tasks, EKS pods, or EC2 instances 3. Omit the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables When running in a properly configured AWS environment, the containers will automatically use the IAM role credentials. ## MongoDB on AWS While AWS DocumentDB is technically compatible with our application, we recommend using native MongoDB (either self-hosted on EC2 or a managed MongoDB Atlas cluster) for better compatibility. Some customers have encountered edge cases with DocumentDB due to differences in MongoDB API implementation. If you choose to use DocumentDB, be prepared for potential compatibility issues. ## Redis on AWS For Redis, you can use Amazon ElastiCache. Keep in mind that Redis is only used as a cache in our application and can be safely restarted or cleared. There's no persistent data stored in Redis that isn't recoverable from other sources. ### ElastiCache TLS Configuration If you're using ElastiCache with in-transit encryption enabled, set the following environment variable: ```bash theme={null} REDIS_DISABLE_TLS_VERIFICATION=true ``` This is required because ElastiCache uses self-signed certificates that won't pass standard TLS verification. ## EKS Deployment When deploying Membrane on Amazon EKS, you may encounter startup issues due to DNS resolution delays that are common in Kubernetes environments. This section provides recommended configurations and troubleshooting guidance. ### Common Startup Issues **CrashLoopBackOff due to health check failures**: During initial deployment or image upgrades, pods may fail health checks because MongoDB or Redis connections timeout before DNS resolution completes. **Symptoms:** * Pods entering `CrashLoopBackOff` state shortly after startup * Health check endpoint returning failures * Logs showing connection timeouts to MongoDB or Redis ### Recommended EKS Configuration Add these environment variables to your deployment to handle slower DNS resolution in Kubernetes: ```yaml theme={null} env: # Increase MongoDB server selection timeout (default: 30000ms) - name: MONGO_SERVER_SELECTION_TIMEOUT_MS value: '60000' # Increase Redis connection timeout (default: 10000ms for standalone) - name: REDIS_CONNECT_TIMEOUT_MS value: '60000' # Configure health check retry behavior for transient failures - name: HEALTH_CHECK_RETRIES value: '5' # Number of retries (default: 3) - name: HEALTH_CHECK_RETRY_DELAY_MS value: '2000' # Initial delay between retries in ms (default: 1000) - name: HEALTH_CHECK_MAX_RETRY_DELAY_MS value: '30000' # Maximum delay between retries in ms (default: 10000) # Required for ElastiCache with in-transit encryption - name: REDIS_DISABLE_TLS_VERIFICATION value: 'true' # Optional: Skip specific health checks if needed # - name: SKIP_HEALTH_CHECKS # value: "mongo,redis" # Or "all" to skip all checks ``` ### Health Check Retry Configuration Health checks include built-in retry logic with exponential backoff to handle transient network issues common in Kubernetes environments. | Variable | Default | Description | | :- | :- | :- | | `HEALTH_CHECK_RETRIES` | `3` | Number of retry attempts after initial failure | | `HEALTH_CHECK_RETRY_DELAY_MS` | `1000` | Initial delay between retries (milliseconds) | | `HEALTH_CHECK_MAX_RETRY_DELAY_MS` | `10000` | Maximum delay between retries (milliseconds) | The retry mechanism uses exponential backoff with jitter: * First retry: \~1 second delay * Second retry: \~2 seconds delay * Third retry: \~4 seconds delay * And so on, up to the maximum delay For EKS environments with slow DNS resolution, we recommend increasing these values: ```yaml theme={null} env: - name: HEALTH_CHECK_RETRIES value: '5' - name: HEALTH_CHECK_RETRY_DELAY_MS value: '2000' - name: HEALTH_CHECK_MAX_RETRY_DELAY_MS value: '30000' ``` ### Health Check Skip Options If retry logic is insufficient, you can skip specific health checks using the `SKIP_HEALTH_CHECKS` environment variable: | Value | Description | | :- | :- | | `all` | Skip all health checks (MongoDB, Redis, storage, custom code) | | `mongo` | Skip only MongoDB connectivity check | | `redis` | Skip only Redis connectivity check | | `storage` | Skip only cloud storage bucket check | | `custom_code` | Skip only custom code runner check | | `mongo,redis` | Skip multiple checks (comma-separated) | **Note:** Skipping health checks is recommended only as a last resort. The retry mechanism should handle most transient failures. ### Kubernetes Probe Configuration In addition to the environment variables, consider adjusting your Kubernetes probe settings: ```yaml theme={null} startupProbe: httpGet: path: / port: 5000 initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 60 # Allow up to 5 minutes for startup livenessProbe: httpGet: path: / port: 5000 initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: / port: 5000 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 3 ``` ### Troubleshooting **1. Check pod logs for connection errors:** ```bash theme={null} kubectl logs -n ``` Look for timeout errors related to MongoDB or Redis connections. **2. Verify DNS resolution from within the cluster:** ```bash theme={null} kubectl run -it --rm debug --image=busybox --restart=Never -- nslookup ``` **3. Test connectivity to external services:** ```bash theme={null} kubectl run -it --rm debug --image=mongo:latest --restart=Never -- mongosh "mongodb+srv://" --eval "db.runCommand({ping:1})" ``` **4. If issues persist after configuration changes:** * Ensure security groups allow traffic between EKS nodes and your MongoDB/Redis instances * Verify VPC peering or PrivateLink configurations if using cross-VPC connections * Check that IAM roles have appropriate permissions for S3 bucket access # Azure Source: https://docs.getmembrane.com/docs/managing-membrane/self-hosting/azure # Self-hosting on Azure To self-host Membrane on Azure, you need to set up Azure Blob Storage. Everything else is the same as in the [main guide](/docs/managing-membrane/self-hosting). ## Setting up Azure Blob Storage This guide uses terraform to manage the infrastructure. 1. Create a storage account and storage containers: ```hcl theme={null} resource "azurerm_storage_account" "main" { name = "${var.environment}integrationapp" resource_group_name = var.resource_group_name location = var.resource_group_location account_tier = "Standard" account_replication_type = "LRS" min_tls_version = "TLS1_2" cross_tenant_replication_enabled = true blob_properties { cors_rule { allowed_headers = ["*"] allowed_methods = ["GET"] allowed_origins = ["*"] exposed_headers = ["*"] max_age_in_seconds = 3000 } } tags = local.common_tags } # Container for temporary files resource "azurerm_storage_container" "tmp" { name = "integration-app-${var.environment}-temp" storage_account_id = azurerm_storage_account.main.id container_access_type = "private" } # Container for connectors resource "azurerm_storage_container" "connectors" { name = "integration-app-${var.environment}-connectors" storage_account_id = azurerm_storage_account.main.id container_access_type = "private" } # Note: For static website hosting, Azure automatically creates a $web container # We'll use that container for static files instead of creating a separate one # Static website configuration resource "azurerm_storage_account_static_website" "main" { storage_account_id = azurerm_storage_account.main.id index_document = "index.html" error_404_document = "404.html" } # Lifecycle management for tmp container resource "azurerm_storage_management_policy" "tmp" { storage_account_id = azurerm_storage_account.main.id rule { name = "cleanup" enabled = true filters { blob_types = ["blockBlob"] prefix_match = ["tmp/"] } actions { base_blob { delete_after_days_since_modification_greater_than = 7 } } } } ``` 2. Configure CDN FrontDoor for serving static files (Optional) ```hcl theme={null} resource "azurerm_cdn_frontdoor_profile" "static" { name = "${var.environment}-afd-static" resource_group_name = var.resource_group_name sku_name = "Standard_AzureFrontDoor" tags = { Service = "core-azure" } } resource "azurerm_cdn_frontdoor_endpoint" "static" { name = "${var.environment}-afd-static-endpoint" cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.static.id } resource "azurerm_cdn_frontdoor_origin_group" "static" { name = "${var.environment}-afd-static-origin-group" cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.static.id load_balancing { sample_size = 4 successful_samples_required = 3 } } resource "azurerm_cdn_frontdoor_origin" "static" { name = "${var.environment}-afd-static-origin" cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.static.id enabled = true host_name = "${azurerm_storage_account.main.name}.z13.web.core.windows.net" http_port = 80 https_port = 443 origin_host_header = "${azurerm_storage_account.main.name}.z13.web.core.windows.net" priority = 1 weight = 1000 certificate_name_check_enabled = true } resource "azurerm_cdn_frontdoor_route" "static" { name = "${var.environment}-afd-static-route" cdn_frontdoor_endpoint_id = azurerm_cdn_frontdoor_endpoint.static.id cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.static.id cdn_frontdoor_origin_ids = [azurerm_cdn_frontdoor_origin.static.id] enabled = true forwarding_protocol = "HttpsOnly" https_redirect_enabled = true patterns_to_match = ["/*"] supported_protocols = ["Http", "Https"] link_to_default_domain = true cdn_frontdoor_custom_domain_ids = [azurerm_cdn_frontdoor_custom_domain.static.id] cdn_frontdoor_rule_set_ids = [azurerm_cdn_frontdoor_rule_set.static.id] } resource "azurerm_cdn_frontdoor_custom_domain" "static" { name = "staticazureintmembrane" cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.static.id host_name = "static.${var.dns_zone_name}" tls { certificate_type = "ManagedCertificate" } } resource "azurerm_cdn_frontdoor_custom_domain_association" "static" { cdn_frontdoor_custom_domain_id = azurerm_cdn_frontdoor_custom_domain.static.id cdn_frontdoor_route_ids = [azurerm_cdn_frontdoor_route.static.id] } resource "azurerm_cdn_frontdoor_rule_set" "static" { name = "${var.environment}staticrules" cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.static.id } resource "azurerm_cdn_frontdoor_rule" "compression" { name = "enablecompression" cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.static.id order = 1 behavior_on_match = "Continue" conditions { request_method_condition { match_values = ["GET"] operator = "Equal" negate_condition = false } } actions { route_configuration_override_action { compression_enabled = true cache_behavior = "OverrideIfOriginMissing" cache_duration = "1.00:00:00" # 1 day query_string_caching_behavior = "IgnoreQueryString" } } } resource "azurerm_cdn_frontdoor_rule" "cache_static_assets" { name = "cachestaticassets" cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.static.id order = 2 behavior_on_match = "Continue" conditions { request_uri_condition { match_values = ["*.js", "*.css", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.ico", "*.woff", "*.woff2"] operator = "EndsWith" negate_condition = false } } actions { route_configuration_override_action { cache_behavior = "OverrideAlways" cache_duration = "7.00:00:00" # 7 days for static assets query_string_caching_behavior = "IgnoreQueryString" } } } ``` 3. Configure custom domain DNS managed zone for BASE\_STATIC\_URI (Optional) ```hcl theme={null} resource "azurerm_dns_txt_record" "afd_static_validation" { name = "_dnsauth.static" zone_name = var.dns_zone_name resource_group_name = var.resource_group_name ttl = 3600 record { value = azurerm_cdn_frontdoor_custom_domain.static.validation_token } } resource "azurerm_dns_cname_record" "static" { name = "static" zone_name = var.dns_zone_name resource_group_name = var.resource_group_name ttl = 3600 record = azurerm_cdn_frontdoor_endpoint.static.host_name } ``` # Connection Credentials Storage Source: https://docs.getmembrane.com/docs/managing-membrane/self-hosting/connection-credentials-storage By default, Membrane stores connection credentials in the database. If configured, it allows storing those credentials in an external API. ## Configuration ### Database (default) No configuration required. By default this is the storage used (default value: `database`). ### External API You need to set 2 environment variables: 1. `CONNECTION_CREDENTIALS_STORAGE_TYPE` to `external_api` 2. `CONNECTION_CREDENTIALS_EXTERNAL_API_ENDPOINT_URL` to the designated endpoint, using REST API for reading and saving connection credentials. All communication (saving and reading credentials) will pass `X-Membrane-Token` header with your token. ### Saving credentials Endpoint URL will contain `connectionId` query parameter (added by Membrane), as connection identifier. Body payload will contain credentials to store. ```sh theme={null} PUT: https://api.yourendpointurl.com?connectionId={CONNECTION_ID} ``` Hint: URL can contain any other query parameters, or path. `connectionId` parameter will be added to the URL. **Payload:** ```json theme={null} { "credentials": "YOUR_CREDENTIALS" } ``` **Where:** * `CONNECTION_ID` is the ID of the connection you wish to store the credentials for * `YOUR_CREDENTIALS` is any valid JSON value. Can be a string, object, whatever is used for credentials for given connection, e.g.: ```json theme={null} { "accessToken": "test-access-token", "refreshToken": "test-refresh-token", "expiresAt": "2024-12-31T23:59:59Z" } ``` **Expected responses:** * `201 Created` when save was successful * non `2xx` when save was unsuccessful ### Reading credentials Endpoint URL will contain `connectionId` as a query parameter: ```sh theme={null} GET: https://api.yourendpointurl.com?connectionId={CONNECTION_ID} ``` Hint: URL can contain any other query parameters or path. `connectionId` parameter will be added to the URL. **Expected responses:** * `200 OK` with body `{ "credentials": "YOUR_CREDENTIALS" }` * `404 NOT FOUND` when credentials were not found **Where:** * `CONNECTION_ID` is the ID of the connection you wish to get the credentials for * `YOUR_CREDENTIALS` contains previously stored credentials, e.g.: ```json theme={null} { "accessToken": "test-access-token", "refreshToken": "test-refresh-token", "expiresAt": "2024-12-31T23:59:59Z" } ``` # Google Cloud Platform Source: https://docs.getmembrane.com/docs/managing-membrane/self-hosting/google-cloud-platform # Self-hosting on Google Cloud Platform To self-host Membrane on Google Cloud Platform, you need to set up Google Cloud Storage. Everything else is the same as in the [main guide](/docs/managing-membrane/self-hosting). ## Setting up Google Cloud Storage This guide uses terraform to manage the infrastructure. Create a service account: ```hcl theme={null} # Create a Google Service Account for the API service resource "google_service_account" "api_service" { account_id = "${var.project}-${var.environment}-api" display_name = "API Service Account for ${var.environment}" project = var.gcp_project_id } # Grant necessary permissions to the service account resource "google_project_iam_member" "api_storage_admin" { project = var.gcp_project_id role = "roles/storage.admin" member = "serviceAccount:${google_service_account.api_service.email}" } # Grant token creator permission for storage signed URLs resource "google_project_iam_member" "api_token_creator" { project = var.gcp_project_id role = "roles/iam.serviceAccountTokenCreator" member = "serviceAccount:${google_service_account.api_service.email}" } # Allow the Kubernetes service account to impersonate the Google service account resource "google_service_account_iam_binding" "api_workload_identity" { service_account_id = google_service_account.api_service.name role = "roles/iam.workloadIdentityUser" members = [ "serviceAccount:${var.gcp_project_id}.svc.id.goog[${var.kubernetes_namespace}/integration-app]" ] } ``` 2. Create your buckets ```hcl theme={null} # Temporary files bucket resource "google_storage_bucket" "tmp" { name = "${var.project}-${var.environment}-tmp" location = var.gcp_region project = var.gcp_project_id # Force destroy even if bucket contains objects force_destroy = true # Uniform bucket-level access uniform_bucket_level_access = true # Lifecycle rule to delete objects after 7 days lifecycle_rule { condition { age = 7 } action { type = "Delete" } } # Versioning versioning { enabled = false } # Labels labels = local.common_labels } # Connectors bucket resource "google_storage_bucket" "connectors" { name = "${var.project}-${var.environment}-connectors" location = var.gcp_region project = var.gcp_project_id # Prevent accidental deletion force_destroy = false # Uniform bucket-level access uniform_bucket_level_access = true # Versioning for code safety versioning { enabled = true } # Lifecycle rule to delete old versions after 30 days lifecycle_rule { condition { num_newer_versions = 3 with_state = "ARCHIVED" } action { type = "Delete" } } # Labels labels = local.common_labels } # Static content bucket resource "google_storage_bucket" "static" { name = "${var.project}-${var.environment}-static" location = var.gcp_region project = var.gcp_project_id # Force destroy for development environments force_destroy = var.environment != "prod" # Uniform bucket-level access uniform_bucket_level_access = true # Website configuration website { main_page_suffix = "index.html" not_found_page = "404.html" } # CORS configuration cors { origin = ["*"] method = ["GET", "HEAD", "OPTIONS"] response_header = ["*"] max_age_seconds = 3600 } # Versioning versioning { enabled = false } # Labels labels = local.common_labels } # Make static bucket publicly accessible for CDN (Optional) resource "google_storage_bucket_iam_member" "static_public_access" { bucket = google_storage_bucket.static.name role = "roles/storage.objectViewer" member = "allUsers" } ``` 3. Configure CDN for serving static files (Optional) ```hcl theme={null} # Reserve a global static IP for the load balancer resource "google_compute_global_address" "cdn_ip" { count = var.enable_cdn ? 1 : 0 name = "${var.project}-${var.environment}-cdn-ip" project = var.gcp_project_id } # Backend bucket for serving static content resource "google_compute_backend_bucket" "static_backend" { count = var.enable_cdn ? 1 : 0 name = "${var.project}-${var.environment}-static-backend" description = "Backend bucket for static content" bucket_name = google_storage_bucket.static.name project = var.gcp_project_id # Enable CDN enable_cdn = true # CDN policy cdn_policy { cache_mode = "CACHE_ALL_STATIC" # Cache TTLs client_ttl = 3600 default_ttl = 3600 max_ttl = 86400 # Negative caching negative_caching = true negative_caching_policy { code = 404 ttl = 120 } # Cache key policy cache_key_policy { query_string_whitelist = [] } } # Compression compression_mode = "AUTOMATIC" } # URL map for routing resource "google_compute_url_map" "cdn_url_map" { count = var.enable_cdn ? 1 : 0 name = "${var.project}-${var.environment}-cdn-url-map" default_service = google_compute_backend_bucket.static_backend[0].id project = var.gcp_project_id # Host rules for custom domains host_rule { hosts = ["static.${var.domain_name}"] path_matcher = "static-paths" } # Path matchers path_matcher { name = "static-paths" default_service = google_compute_backend_bucket.static_backend[0].id path_rule { paths = ["/api/*"] service = google_compute_backend_bucket.static_backend[0].id route_action { url_rewrite { path_prefix_rewrite = "/" } } } } } # Managed SSL certificate resource "google_compute_managed_ssl_certificate" "cdn_cert" { count = var.enable_cdn ? 1 : 0 name = "${var.project}-${var.environment}-cdn-cert" project = var.gcp_project_id managed { domains = ["static.${var.domain_name}"] } } # HTTPS proxy resource "google_compute_target_https_proxy" "cdn_https_proxy" { count = var.enable_cdn ? 1 : 0 name = "${var.project}-${var.environment}-cdn-https-proxy" url_map = google_compute_url_map.cdn_url_map[0].id ssl_certificates = [google_compute_managed_ssl_certificate.cdn_cert[0].id] project = var.gcp_project_id } # HTTP proxy (for redirect to HTTPS) resource "google_compute_target_http_proxy" "cdn_http_proxy" { count = var.enable_cdn ? 1 : 0 name = "${var.project}-${var.environment}-cdn-http-proxy" url_map = google_compute_url_map.cdn_http_redirect[0].id project = var.gcp_project_id } # URL map for HTTP to HTTPS redirect resource "google_compute_url_map" "cdn_http_redirect" { count = var.enable_cdn ? 1 : 0 name = "${var.project}-${var.environment}-cdn-http-redirect" project = var.gcp_project_id default_url_redirect { https_redirect = true redirect_response_code = "MOVED_PERMANENTLY_DEFAULT" strip_query = false } } # Global forwarding rule for HTTPS resource "google_compute_global_forwarding_rule" "cdn_https_forwarding" { count = var.enable_cdn ? 1 : 0 name = "${var.project}-${var.environment}-cdn-https-forwarding" ip_protocol = "TCP" load_balancing_scheme = "EXTERNAL_MANAGED" port_range = "443" target = google_compute_target_https_proxy.cdn_https_proxy[0].id ip_address = google_compute_global_address.cdn_ip[0].id project = var.gcp_project_id } # Global forwarding rule for HTTP resource "google_compute_global_forwarding_rule" "cdn_http_forwarding" { count = var.enable_cdn ? 1 : 0 name = "${var.project}-${var.environment}-cdn-http-forwarding" ip_protocol = "TCP" load_balancing_scheme = "EXTERNAL_MANAGED" port_range = "80" target = google_compute_target_http_proxy.cdn_http_proxy[0].id ip_address = google_compute_global_address.cdn_ip[0].id project = var.gcp_project_id } # Cloud Armor security policy (optional, for DDoS protection) resource "google_compute_security_policy" "cdn_security_policy" { count = var.enable_cdn && var.environment == "prod" ? 1 : 0 name = "${var.project}-${var.environment}-cdn-security-policy" project = var.gcp_project_id # Default rule rule { action = "allow" priority = "2147483647" match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = ["*"] } } description = "Default rule, allow all traffic" } # Rate limiting rule rule { action = "rate_based_ban" priority = "1000" match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = ["*"] } } rate_limit_options { conform_action = "allow" exceed_action = "deny(429)" enforce_on_key = "IP" rate_limit_threshold { count = 1000 interval_sec = 60 } } description = "Rate limit rule" } } ``` 4. Configure custom domain DNS managed zone for BASE\_STATIC\_URI (Optional) ```hcl theme={null} # Create DNS managed zone resource "google_dns_managed_zone" "main" { name = var.dns_zone_name dns_name = "${var.domain_name}." description = "${var.project} ${var.environment} DNS zone" project = var.gcp_project_id labels = local.common_labels dnssec_config { state = "on" } } # DNS record for static content CDN resource "google_dns_record_set" "static_cdn" { count = var.enable_cdn ? 1 : 0 name = "static.${google_dns_managed_zone.main.dns_name}" managed_zone = google_dns_managed_zone.main.name type = "A" ttl = 300 project = var.gcp_project_id rrdatas = [google_compute_global_address.cdn_ip[0].address] } # TXT record for domain verification resource "google_dns_record_set" "domain_verification" { name = google_dns_managed_zone.main.dns_name managed_zone = google_dns_managed_zone.main.name type = "TXT" ttl = 300 project = var.gcp_project_id rrdatas = [ "\"v=spf1 -all\"", "\"google-site-verification=${var.google_site_verification_code}\"" ] } # A record for the root domain (optional) resource "google_dns_record_set" "root" { count = var.enable_cdn ? 1 : 0 name = google_dns_managed_zone.main.dns_name managed_zone = google_dns_managed_zone.main.name type = "A" ttl = 300 project = var.gcp_project_id rrdatas = [google_compute_global_address.cdn_ip[0].address] } # CAA record for SSL certificate issuance resource "google_dns_record_set" "caa" { name = google_dns_managed_zone.main.dns_name managed_zone = google_dns_managed_zone.main.name type = "CAA" ttl = 300 project = var.gcp_project_id rrdatas = [ "0 issue \"pki.goog\"", "0 issue \"letsencrypt.org\"" ] } ``` # Helm Source: https://docs.getmembrane.com/docs/managing-membrane/self-hosting/helm ## Registry Access You'll need credentials from our support team to access Membrane artifacts: **Registry Credentials** * Username format: `robot$` * Access to: `harbor.getmembrane.com`
### Setting Up Registry Access 1. Login to Helm registry ``` helm registry login harbor.getmembrane.com \ --username \ --password ``` 2. Pull and unpack the Integration.app Helm chart: ``` # See Versions section at the bottom of this page for available versions helm pull oci://harbor.getmembrane.com/helm/integration-app --version --untar ``` ## Prerequisites Before installing Membrane using Helm, ensure you have the following components set up: ### Prometheus Stack The kube-prometheus stack provides Prometheus, Grafana dashboards, and necessary Prometheus rules: ``` helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update kubectl create namespace monitoring helm install prometheus-stack prometheus-community/kube-prometheus-stack --namespace monitoring ``` For advanced configuration options, refer to the [kube-prometheus stack documentation](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack/#kube-prometheus-stack). ### KEDA If you plan to use autoscaling features, install KEDA: ``` helm repo add kedacore https://kedacore.github.io/charts helm repo update kubectl create namespace keda helm install keda kedacore/keda --namespace keda ``` For advanced KEDA configuration, consult the [official KEDA documentation](https://keda.sh/docs/2.15/deploy/). ## Installation 1. **Configure Container Registry Access** Create a Docker registry secret using your container registry credentials: ``` kubectl create secret docker-registry integration-app-harbor \ --namespace \ --docker-server=harbor.getmembrane.com \ --docker-username= \ --docker-password= ``` 2. **Prepare Configuration** Populate or provide override to `values.yaml` file with the values for your setup: ```Text yaml theme={null} serviceAccount: annotations: eks.amazonaws.com/role-arn: arn:aws:iam:::role/ config: NODE_ENV: production MONGO_URI: MONGO_URI REDIS_URI: REDIS_URI # Self-hosting token (required) - generate from https://console.getmembrane.com SELF_HOSTING_TOKEN: SELF_HOSTING_TOKEN # URI where `api` service will be available API_URI: API_URI # URI where `ui` service will be available UI_URI: UI_URI # URI where `console` service will be available CONSOLE_URI: CONSOLE_URI # Bucket for storing custom connectors CONNECTORS_S3_BUCKET: CONNECTORS_S3_BUCKET # Bucket for storing temporary files (like logs) TMP_S3_BUCKET: TMP_S3_BUCKET # Buckets for storing static files that should be available in user's browser (like images) STATIC_S3_BUCKET: STATIC_S3_BUCKET # Base URI where files stored in STATIC_S3_BUCKET will be available BASE_STATIC_URI: BASE_STATIC_URI # Auth0 Settings AUTH0_DOMAIN: AUTH0_DOMAIN AUTH0_CLIENT_ID: AUTH0_CLIENT_ID AUTH0_CLIENT_SECRET: AUTH0_CLIENT_SECRET # Secret key used for signing JWT tokens SECRET: SECRET # Secret key used for encrypting data at rest ENCRYPTION_SECRET: ENCRYPTION_SECRET ``` 3. **Validate Chart** Before deploying, make sure that chart is rendering correctly: ``` helm template integration-app ./path-to-your-chart --namespace ``` 4. **Select Cluster Context** Make sure to switch to desired cluster context: ``` kubectl config use-context ``` 5. **Deploy** Install the chart to cluster: ``` helm install integration-app ./path-to-this-folder --namespace --create-namespace ``` To update an existing installation: ``` helm upgrade integration-app ./path-to-this-folder --namespace ``` ## Autoscaling Configuration The following components support autoscaling: * API * Instant Tasks Worker * Queued Tasks Worker * Custom Code Runner Each component that supports autoscaling accepts these parameters: | Parameter | Type | Description | | :- | :- | :- | | `.autoscaling.enabled` | Boolean | Enables/disables autoscaling for the component. If autoscaling is a number of replicas will taken from`.replicas` property. IF autoscaling is enabled, `.replicas` is ignored. | | `.autoscaling.minReplicaCount` | Number | Minimum number of replicas | | `.autoscaling.maxReplicaCount` | Number | Maximum number of replicas | | `.autoscaling.cooldownPeriod` | Number | Cooldown period between scaling operations | | `.autoscaling.pollingInterval` | Number | How often to check scaling metrics | These properties are part of KEDA's core functionality. For more detailed information, please refer to the [official KEDA documentation](https://keda.sh/docs/2.14/reference/scaledobject-spec/). ### Component-Specific Scaling Each component has specific scaling parameters that control its autoscaling behavior: | Parameter | Type | Default | Description | | :- | :- | :- | :- | | `api.autoscaling. scalingTargets. cpuUtilizationPercent` | Number | 50 | Defines the target CPU utilization percentage. Adjusting this value will influence how aggressively the API scales in response to CPU load | | `instantTasksWorker. autoscaling. scalingTargets. utilizationRate` | Number | 0.75 | Defines the expected percentage of time (0.0-1.0) that workers should be actively processing tasks. Higher values minimize worker idle time but can cause processing delays during high load periods | | `customCodeRunner. autoscaling. scalingTargets. capacityRate` | Number | 0.45 | Defines the capacity rate of available to total slots. A higher value increases the likelihood of custom code execution waiting for a slot, potentially slowing down API requests. A lower value ensures that custom code requests are processed promptly, but it may result in a higher number of idle pods. | | `queuedTasksWorker. autoscaling. scalingTargets. utilizationRate` | Number | 0.85 | Defines the expected percentage of time (0.0-1.0) that workers should be actively processing tasks. Higher values minimize worker idle time but can cause processing delays during high load periods |
## Versions and Changelog ### Latest Version: 0.2.4 Version Release Date Changes 0.2.4 2025-12-04 Simplified autoscaling for instant-tasks-worker and queued-tasks-worker to use utilizationRate-based metrics with Prometheus queries. Added Prometheus scraping annotations and service for instant-tasks-worker. 0.2.3 2025-10-21 Optimized graceful shutdown timing and improved health check reliability for better pod lifecycle management 0.2.2 2025-10-09 Added startup probe configuration with 5s period and 60s failure threshold across all services for improved container initialization handling. Updated deployment strategy with maxSurge: 50% and maxUnavailable: 25% for better control during rolling updates. 0.2.1 2025-06-01 Initial release of the Helm chart Support for all core services KEDA autoscaling configuration Prometheus metrics integration # Self-hosted Universe Source: https://docs.getmembrane.com/docs/managing-membrane/self-hosting/self-hosted-universe Membrane Universe gives self-hosted instances access to the same library of connectors, apps, and actions available on Membrane Cloud. Your self-hosted instance fetches these elements from the Membrane Cloud API on demand. ## Configuration Membrane Universe requires two environment variables on the API service: | Variable | Description | Example | | :- | :- | :- | | `SELF_HOSTING_TOKEN` | Your self-hosting token. Generate from your [organization settings](https://console.getmembrane.com/w/0/settings/organization/self-hosting-tokens). | `sht_abc123...` | | `CLOUD_API_URL` | Membrane Cloud API URL. | `https://api.getmembrane.com` | Both variables must be set for Membrane Universe to be enabled. ## Available Elements When Membrane Universe is enabled, the following elements from Membrane Cloud become available on your self-hosted instance: * **Connectors** — pre-built connectors with their auth specs, operations, and data collections. Available automatically. * **External Apps** — the full app catalog with logos, categories, and metadata. Available automatically. * **Actions** — pre-built actions from app packages. Requires the **Enable Action Packages** workspace setting (see below). ## Enable Action Packages The **Enable Action Packages** setting in workspace settings controls whether pre-built actions from app packages appear in action listings. Connectors and external apps are available automatically regardless of this setting. Actions require it because they are loaded from public packages, which may affect the number of elements shown in listings. # System Webhooks Source: https://docs.getmembrane.com/docs/managing-membrane/self-hosting/system-webhooks Membrane supports system-level webhooks that allow you to receive notifications about platform events. Webhooks are managed through the Admin Console and require admin permissions to configure. ### Webhook Types Membrane supports the following webhook types: #### User Invited to Organization (`user-invited-to-org`) Triggered when a user is invited to join an organization workspace. **Payload Example:** ```json theme={null} { "type": "user-invited-to-org", "invitationUrl": "https://console.yourdomain.com/org-invitations/12345", "issuer": { "name": "John Admin", "email": "admin@company.com" }, "user": { "email": "newuser@company.com" }, "org": { "id": "org123", "name": "My Organization", "trialEndDate": "2025-05-16" } } ``` #### Organization Access Requested (`org-access-requested`) Triggered when a user requests access to an organization based on their email domain. **Payload Example:** ```json theme={null} { "type": "org-access-requested", "user": { "id": "user123", "email": "employee@company.com", "name": "Jane Employee" }, "orgAdmins": [ { "email": "admin@company.com", "orgs": [ { "id": "org123", "name": "My Organization" } ] } ] } ``` #### Organization Created (`org-created`) Triggered when a new organization is created on the platform. **Payload Example:** ```json theme={null} { "type": "org-created", "name": "New Organization", "workspaceName": "Main Workspace", "orgId": "org456", "org": { "id": "org456", "name": "New Organization", "domains": ["company.com"], "trialEndDate": "2025-05-23" }, "user": { "name": "Creator User", "email": "creator@company.com" } } ``` ### Webhook Authentication Webhooks support HMAC-SHA256 signature verification for secure payload validation: 1. **Secret Configuration**: When creating a webhook, optionally provide a `secret` string 2. **Signature Generation**: Membrane generates an HMAC-SHA256 signature using the webhook secret 3. **Header Delivery**: The signature is sent in the `X-Signature` header with each webhook request 4. **Verification**: Verify the payload by generating the same HMAC signature on your end **Example Verification (Node.js):** ```javascript theme={null} const crypto = require('crypto') function verifyWebhook(payload, signature, secret) { const expectedSignature = crypto.createHmac('sha256', secret).update(JSON.stringify(payload)).digest('hex') return signature === expectedSignature } // In your webhook handler app.post('/webhook', (req, res) => { const signature = req.headers['x-signature'] const isValid = verifyWebhook(req.body, signature, 'your-webhook-secret') if (!isValid) { return res.status(401).send('Invalid signature') } // Process webhook payload res.status(200).send('OK') }) ``` ### Managing Webhooks Webhooks can be managed through the Admin Console: 1. Navigate to **Admin** > **Manage Webhooks** 2. Click **Create Webhook** 3. Configure: * **Type**: Select the webhook event type * **URL**: Your webhook endpoint URL * **Secret**: Optional secret for HMAC verification **API Endpoints:** * `GET /webhooks` - List all webhooks * `POST /webhooks` - Create a new webhook * `GET /webhooks/:type` - Get webhook by type * `PATCH /webhooks/:type` - Update webhook * `DELETE /webhooks/:type` - Delete webhook > **Note**: All webhook management operations require admin authentication via Auth0 JWT tokens. ### Webhook Configuration Requirements * **Admin Access**: Only platform administrators can create, modify, or delete webhooks * **Unique Types**: Only one webhook can be configured per event type * **HTTPS Required**: Webhook URLs should use HTTPS for security * **Response Timeout**: Webhook requests have a 30-second timeout * **Retry Policy**: Failed webhooks are not automatically retried ### Testing Webhooks For development and testing purposes, you can use tools like: * **ngrok**: To expose local development servers * **webhook.site**: To inspect webhook payloads * **Postman**: To simulate webhook endpoints Example ngrok setup: ```bash theme={null} # Install ngrok npm install -g ngrok # Expose local server ngrok http 3000 # Use the generated HTTPS URL in webhook configuration ``` ### Webhook handler examples There is [a public repository](https://github.com/membranehq/admin-webhook-handler) with examples for webhook handler functions # Data Schemas Source: https://docs.getmembrane.com/docs/references/data-schemas Data Schema is used whenever you need to describe the data structure without providing the actual value. Data Schema we use is based on [JSONSchema](https://json-schema.org/) with additional features. ## Additional Properties ### Reference Records (Enums or Picklists): To specify reference records that can be used for a field (for example, to create a field with limited options for selection) use schema like this: ```yaml theme={null} status: type: number # Type of `id` keys below referenceRecords: - name: Open # Title of that option displayed in the UI id: 1 # Actual value that will be used after selecting an option - name: Won id: 2 - name: Lost id: 3 ``` ### Reference Collection When working in a context of a [Connection](/docs/how-membrane-works/connections), you can point to a specific [Data Collection](/reference/workspace-elements/data-collections) to use its records as possible values for a field. Example: ```yaml theme={null} ownerId: type: string referenceCollection: key: users parameters: isActive: true ``` ### Is Sensitive There is an additional property `isSensitive` (boolean) that can be used to specify that the field contains sensitive information and should be treated accordingly (hidden in the UI, excluded from logs, etc). Example: ```yaml theme={null} password: type: string isSensitive: true ``` ## Dynamic Data Schema You can use [Formulas](/docs/references/formulas) in data schemas to make them dynamic. For example: ```yaml theme={null} parent: type: object properties: type: type: string enum: ['tasks', 'projects'] id: type: string referenceCollection: key: $var: parent.type ``` In this, `parent.id` property will reference the `tasks` collection if `parent.type` is `tasks` and the `projects` collection if `parent.type` is `projects`. Schema will be evaluated in the context of the current value the schema is applied to. It works in a special way for `array` schemas: when evaluating dynamic schemas, all arrays leading to the current value will be replaced with the current item that is being evaluated. For example: ```yaml theme={null} parents: type: array items: type: object properties: type: type: string enum: ['tasks', 'projects'] id: type: string referenceCollection: key: $var: parents.type ``` Notice the locator in the `id` field: `parents.type`. It treats `parents` as if it was an object rather than array. When evaluated, it will use the current item of the `parents` array. For example, if the schema is applied to the following object: ```yaml theme={null} parents: - type: tasks id: 1 - type: projects ``` The schema for the first item of the `parents` list will be the following: ```yaml theme={null} type: object properties: type: type: string enum: ['tasks', 'projects'] id: type: string referenceCollection: key: tasks ``` But for the second item, it will be: ```yaml theme={null} type: object properties: type: type: string enum: ['tasks', 'projects'] id: type: string referenceCollection: key: projects ``` # Formulas Source: https://docs.getmembrane.com/docs/references/formulas You can use formulas to dynamically generate values when building [Flows](/reference/workspace-elements/flows), configuring [Field Mappings](/reference/workspace-elements/field-mappings), and in many other places that require data based on variables. Formulas look like this: ```yaml theme={null} fieldFromVariable: $var: $.someVariable mappedField: $map: value: $.someVariable mapping: - from: From Value 1 to: To Value 2 - from: From Value 2 to: To Value 2 ``` ## Variables All formulas use variables to calculate. The simplest formula just references the variable: ```yaml theme={null} fieldFromVariable: $var: $.someVariable ``` `$.someVariable` here is called a Locator. Locators reference a value within the variables structure. You can use `.` to address a field of an object and `[]` to address an index in an array. If fields of an object have `.` or `[` inside them, you can escape them with `\`. For example, `$.dot\.field.bracket\[field[0]` will point to the first element in the field `bracket[field` array inside the `dot.field` variable. ## List of Formulas ### \$concat Concatenates two or more strings. Example: ```yaml theme={null} name: $concat: values: - $var: $.firstName - $var: $.lastName delimiter: ' ' ``` Or a shorter version: ```yaml theme={null} name: $concat: - $var: $.firstName - ' ' - $var: $.lastName ``` ### \$tpl Generates a string with interpolated variables. Example: ```yaml theme={null} subject: $tpl: template: 'Hello, {firstName} {lastName}!' values: firstName: $var: input.firstName lastName: $var: input.lastName ``` ### \$lookup Lets you find a record in a related data collection or create one if it does not exist yet. Example: ```yaml theme={null} contactId: $lookup: query: email: john.doe@example.com createIfNotFound: true create: fields: name: John Doe email: john.doe@example.com ``` It has the following parameters: * `query` - one or more fields used to find a matching related record. * `createIfNotFound` - if set to `true`, a new record will be created if no matching record is found. * `create` * `fields` - a set of fields to use when creating a new record.
**Lookups with dynamic schemas** Lookups support dynamic schemas (see [Data Schemas](/docs/references/data-schemas) for what it is), but with restrictions. Say, your schema looks like this: ```yaml theme={null} type: object properties: referenceType: type: string referenceId: type: string referenceCollection: key: $var: referenceType ``` It has a reference to a collection determined dynamically. Lookups will work for this schema, but only if variables are provided statically. For example, this lookup will work: ```yaml theme={null} referenceType: contacts referenceId: $lookup: query: email: $var: $.email ``` But this lookup will not work because referenceType is dynamic and cannot be used to resolve referenceId lookup: ``` referenceType: $var: $.type referenceId: $lookup: query: email: $var: $.email ```
### \$eval This formula evaluates a value using one of supported operators and returns `true` or `false` depending on the result. Example: ```yaml theme={null} condition: $eval: $var: status is: Open ``` All \$eval formulas have the same form: ```yaml theme={null} $eval: : ``` This \$eval formula will return `true` if the `status` variable has value `Open`. Supported operators: * `is` - value is equal to the operand. * `isNot` - value is not equal to the operand * `isEmpty` - value is empty (null, undefined, empty string, empty array, object without keys). Operand is ignored. * `isNotEmpty` - value is not empty. Operand is ignored. * `contains` - value contains the operand. For strings, it checks if the string contains the provided substring. For arrays, it checks if the array contains the provided value. * `doesNotContain` - value does not contain the operand. * `oneOf` - value is one of the values in the operand array. * `noneOf` - value is none of the values in the operand array. * `gt` - value is greater than the operand. * `gte` - value is greater than or equal to the operand. * `lt` - value is less than operand. * `lte` - value is less than or equal to the operand. ### \$and Returns true if all of its arguments are true Example: ```yaml theme={null} condition: $and: - $eval: $var: status is: Open - $var: isActive ``` It will resolve to `true` if `status` variable is set to `Open` and `isActive` variable is `true`. ### \$or Returns `true` if at least one of its arguments is `true`, otherwise returns `false`. Example: ```yaml theme={null} condition: $or: - $eval: $var: status is: Open - $var: isActive ``` This will return `true` if `status` variable is set to `Open` or `isActive` variable is `true`. ### \$case Case formula selects a value based on a list of conditions. It has the following structure: ```yaml theme={null} conditionalValue: $case: cases: # A list of cases, # each containing optional `filter` and `value` - filter: # Filter is any formula that can resolve into thruth-y or falsy value. $eval: $var: status is: Won # If all the conditions are met, this value will be used. value: 'Status is Won' # Value without `filter` acts as "else". It will always be used # if none of the previous filters matched. - value: 'Status is not Won' # If no cases match - this value is used default: 'Default Value' ``` * If filters of multiple cases match, the first one will be used * A case without a filter always matches. * Filter with zero conditions always matches. ### \$map Map formula transforms a set of input values into corresponding output values. ```yaml theme={null} mappedValue: $map: # Value that will be checked against the list of mappings below value: $var: $.status mapping: # Each mapping item has `from` and `to`. # If `value` above matches `from`, # then `to` will be used as the output of the formula. - from: ToDo to: Open - from: In Progress to: Open - from: Done to: Closed # If none of the mappings matched, then default value will be used. # If `default` is not specified and no mapping matched, # the output will be empty. default: 'Unknown Status' ``` ### \$iterate Iterates over an array and returns an array of values calculated for each item. Example: ```yaml theme={null} transformedContacts: $iterate: source: $var: $.contacts item: name: $var: $.item.name tags: $var: $.parent.tags ``` The formula accepts two arguments: * source: the source list to iterate over. Could be a value or any formula that returns an array. * item: value for each item in the new list. When item value is calculated, three variables are available for it: * item - the current item in the source list. * index - the zero-based index of the current item. * parent - all the variables of the parent scope (i.e. variables available to the \$iterate formula itself). The resulting list will contain one `item` for each item of the source list. ### \$firstName Extract first name from the name. Example: ```yaml theme={null} firstName: $firstName: $var: fullName ``` ### \$lastName Extract last name from the name. Example: ```yaml theme={null} lastName: $lastName: $var: fullName ``` > Note: we will treat single-word names as Last Name because some applications require Last Name, but not First Name. This logic leads to fewer validation errors. ### \$copy Copies another field from your value. Useful when you need the same value in two places and don't want to define it twice. ```yaml theme={null} # Value firstName: $firstName: $var: fullName preferredName: $copy: firstName # Variables fullName: John Snow # Result firstName: John preferredName: John ``` ### \$findExternalRecordId Finds id of a record in an external application matching record in your application using a provided [Data Link Table](/reference/workspace-elements/data-links). ```yaml theme={null} # Will find a link in "person-to-lead" data link table # with appRecordId equal 'leadId'. # Will return externalRecordId from the link. externalPersonId: $findExternalRecordId: dataLinkTable: key: person-to-lead recordId: 'leadId' ``` ### \$findAppRecordId Finds id of a record in you app that matches an external record id using a provided [Data Link Table](/reference/workspace-elements/data-links). ```yaml theme={null} # Will find a link in "person-to-lead" data link table # with externalRecordId equal 'personId'. # Will return appRecordId from the link. appLeadId: $findAppRecordId: dataLinkTable: key: person-to-lead recordId: 'personId' ``` ### \$firstNotEmpty Returns the first non-null, non-undefined value from a list of candidates. ```yaml theme={null} displayName: $firstNotEmpty: - $var: $.preferredName - $var: $.firstName - $var: $.email ``` ### \$mergeObjects Merges multiple objects into one. Later objects override earlier ones (shallow merge). ```yaml theme={null} result: $mergeObjects: - $var: $.defaults - $var: $.overrides ``` ### \$domainFromEmail Extracts the domain part from an email address. ```yaml theme={null} # Input: "jane@example.com" → Output: "example.com" domain: $domainFromEmail: $var: $.email ``` ### \$extractDate Extracts the date portion (YYYY-MM-DD) from a datetime value. ```yaml theme={null} date: $extractDate: $var: $.createdAt ``` ### \$extractTime Extracts the time portion (HH:MM:SS) from a datetime value. ```yaml theme={null} time: $extractTime: $var: $.createdAt ``` ### \$dataSchemaRef > This formula is **experimental**. Breaking changes are possible. Used inside [Data Schemas](/docs/references/data-schemas) to reference another data schema. Example: ```yaml theme={null} type: object title: Update Contact Payload properties: id: type: string fields: $dataSchemaRef: type: data-collection key: contacts ``` In this example, the `fields` property of the schema will be equal to the fieldsSchema of the `contacts` data collection. Which data schemas types are available depends on the context this formula is used in. **Internal Data Schemas** You can reference an [Internal Data Schema](/reference/workspace-elements/app-data-schemas) this way: ```yaml theme={null} $dataSchemaRef: type: internal-data-schema key: contact ``` This schema type is always available. If used in a context of a specific tenant, it will use the tenant-specific fields. **Data Collections** You can reference a `fieldsSchema` of a [Data Collection](/reference/workspace-elements/data-collections) this way: ```yaml theme={null} $dataSchemaRef: type: data-collection key: contacts parameters: listId: 10 ``` This schema type is available in a context of a specific [Connection](/docs/how-membrane-works/connections). It will resolve into data collection fields for a given connection, including any custom fields. ### \$jsonata If you want to apply complex data transformations, we support [Jsonata](https://jsonata.org/) expressions. Example: ```yaml theme={null} sumOfPrices: $jsonata: $sum($.products.price) ``` ### \$plain This formula preserves everything inside it as is. It is useful when you need to skip evaluating formulas in a part of the expression. ```yaml theme={null} # Formula $plain: $firstName: $var: fullName # Result $firstName: $var: fullName ``` ### \$formula This formula allows you to use a formula as a value, but it still evaluates \$var inside it. It is useful when you want to produce a formula dynamically, parametrized by variables. This is, for example, used in connectors when mapping fields to/from unified fields when you want the result to be a formula. ```yaml theme={null} # When mapping unified fields to application-specific fields # we want the result to be a $firstNotEmpty formula, # but we want $.contactId and $.companyId to be replaced with actual values of unified fields. unifiedFieldsToNative: parent: $formula: $firstNotEmpty: - $var: $.contactId - $var: $.companyId # Variables contactId: $var: $.input.contactId companyId: $var: $.input.companyId # Result unifiedFieldsToNative: parent: $firstNotEmpty: - $var: $.input.contactId - $var: $.input.companyId ``` # Functions Source: https://docs.getmembrane.com/docs/references/functions Membrane Functions implement business logic inside Membrane Elements. ## Function Types Functions can be implemented in one of the following ways depending on where the function is used: ### Core Implementation Function Types * [Mapping](/docs/references/functions/function-types/mapping) – Transform input data using JSONata expressions without making API calls * [REST API Mapping](/docs/references/functions/function-types/rest-api-mapping) – Map to REST API endpoints * [JavaScript](/docs/references/functions/function-types/javascript) – Write custom code for maximum flexibility * [GraphQL API Mapping](/docs/references/functions/function-types/graphql-api-mapping) – Map to GraphQL API endpoints ### Data Collection Action Function Types * [list-data-records](/docs/references/functions/function-types/list-data-records) – List records in a data collection page by page * [find-data-record-by-id](/docs/references/functions/function-types/find-data-record-by-id) – Find a record in a data collection by ID * [search-data-record](/docs/references/functions/function-types/search-data-record) – Search data records by a query string * [match-data-record](/docs/references/functions/function-types/match-data-record) – Find a data record with fields matching a query * [create-data-record](/docs/references/functions/function-types/create-data-record) – Create a new data record * [update-data-record](/docs/references/functions/function-types/update-data-record) – Update an existing data record * [delete-data-record](/docs/references/functions/function-types/delete-data-record) – Delete a data record by id ### API Request Action Function Types * [api-request-to-external-app](/docs/references/functions/function-types/api-request-to-external-app) – Make API requests to the external service * [api-request-to-your-app](/docs/references/functions/function-types/api-request-to-your-app) – Make API requests to your internal API * [http-request](/docs/references/functions/function-types/http-request) – Make arbitrary HTTP requests to any endpoint ### Code Execution Action Function Types * [JavaScript](/docs/references/functions/function-types/javascript) – Run custom JavaScript/TypeScript code (includes `run-javascript` action type) ### Flow Node Function Types * [run-action](/docs/references/functions/function-types/run-action) – Run predefined actions ### Special Function Types * `not-supported` – Mark function as not supported by the external API # Function Types Source: https://docs.getmembrane.com/docs/references/functions/function-types Function types determine how integration logic is implemented. Each [Action](/docs/how-membrane-works/actions) and [Flow](/reference/workspace-elements/flows) node uses a function type. | Function Type | Description | | - | - | | [JavaScript](/docs/references/functions/function-types/javascript) | Run custom JavaScript/TypeScript logic | | [Mapping](/docs/references/functions/function-types/mapping) | Transform data using declarative field mappings | | [REST API Mapping](/docs/references/functions/function-types/rest-api-mapping) | Map fields to REST API request parameters | | [GraphQL API Mapping](/docs/references/functions/function-types/graphql-api-mapping) | Map fields to GraphQL queries and mutations | | [API Request to External App](/docs/references/functions/function-types/api-request-to-external-app) | Make authenticated requests to a connected app's API | | [API Request to Your App](/docs/references/functions/function-types/api-request-to-your-app) | Make requests to your backend's Internal API | | [HTTP Request](/docs/references/functions/function-types/http-request) | Make arbitrary HTTP requests | | [List Data Records](/docs/references/functions/function-types/list-data-records) | List records from a data collection | | [Find Data Record By ID](/docs/references/functions/function-types/find-data-record-by-id) | Find a specific record by its ID | | [Search Data Record](/docs/references/functions/function-types/search-data-record) | Search records by query string | | [Match Data Record](/docs/references/functions/function-types/match-data-record) | Find a record matching specific criteria | | [Create Data Record](/docs/references/functions/function-types/create-data-record) | Create a new record in a data collection | | [Update Data Record](/docs/references/functions/function-types/update-data-record) | Update an existing record | | [Delete Data Record](/docs/references/functions/function-types/delete-data-record) | Delete a record | | [Run Action](/docs/references/functions/function-types/run-action) | Execute another action | # API Request to External App Source: https://docs.getmembrane.com/docs/references/functions/function-types/api-request-to-external-app The `api-request-to-external-app` function type makes arbitrary API requests to the API of the current external app with authentication applied automatically. ## Usage This function type is used in Actions to make authenticated API requests to external applications. ## Configuration Example ```yaml theme={null} name: Create Issue key: create-issue inputSchema: type: object properties: projectId: type: string title: type: string priority: type: string type: api-request-to-external-app config: request: path: /projects/{projectId}/issues method: POST pathParameters: projectId: $var: $.input.projectId query: notify: true data: summary: $var: $.input.title priority: $var: $.input.priority headers: X-API-Version: '2025-01-01' Content-Type: application/json responseSchema: type: object properties: id: type: string key: type: string allowNon2xx: false ``` ## Configuration Parameters * `request.path` - API endpoint path (supports `{paramName}` substitution) * `request.method` - HTTP method (GET, POST, PUT, DELETE, etc.) - defaults to GET * `request.pathParameters` - Path parameter substitutions * `request.query` - Query parameters * `request.data` - Request body * `request.headers` - Additional headers * `responseSchema` - Expected response schema (optional) * `allowNon2xx` - Allow non-200 responses (default: false) ## Output Schema ```yaml theme={null} type: object properties: status: type: number data: # Based on responseSchema or generic object ``` # API Request to Your App Source: https://docs.getmembrane.com/docs/references/functions/function-types/api-request-to-your-app The `api-request-to-your-app` function type makes API requests to your [Internal API](/docs/how-membrane-works/internal-api). ## Usage This function type is used in Actions to make requests to your own application's API endpoints. ## Configuration Example ```yaml theme={null} name: Notify Internal System key: notify-internal inputSchema: type: object properties: userId: type: string message: type: string type: api-request-to-your-app config: request: uri: /notifications method: POST body: userId: $var: $.input.userId message: $var: $.input.message headers: Content-Type: application/json batchSize: 10 ``` ## Configuration Parameters * `request.uri` - Internal API endpoint URI * `request.method` - HTTP method (defaults to GET) * `request.body` - Request body * `request.query` - Query parameters * `request.headers` - HTTP headers * `batchSize` - Batch processing size (optional) * `responseSchema` - Expected response schema (optional) * `allowNon2xx` - Allow non-200 responses (optional) ## Output Schema ```yaml theme={null} type: object properties: response: type: object properties: status: type: integer data: # Based on responseSchema ``` # Create Data Record Source: https://docs.getmembrane.com/docs/references/functions/function-types/create-data-record The `create-data-record` function type creates a new data record in a collection. ## Usage This function type is used in Actions to provide access to data collection create functionality. ## Configuration Example ```yaml theme={null} name: Create Task key: create-task inputSchema: type: object properties: title: type: string description: type: string type: create-data-record config: dataSource: collectionKey: tasks fieldMapping: exportValue: summary: $var: $.input.title description: $var: $.input.description status: 'open' ``` ## Configuration Parameters * `dataSource` - Data source configuration * `collectionKey` - Identifier of the data collection to operate on * `collectionParameters` - Parameters passed to the collection methods * `udm` - (for universal integrations only) Universal Data Model to write * `fieldMapping` - Field mapping configuration * `exportValue` - Maps fields from your app to the external app format * `defaultUnifiedValue` - (for universal integrations only) Default values for unified field mapping ## Output Schema ```yaml theme={null} type: object properties: id: type: string ``` # Delete Data Record Source: https://docs.getmembrane.com/docs/references/functions/function-types/delete-data-record The `delete-data-record` function type deletes a data record by ID. ## Usage This function type is used in Actions to provide access to data collection delete functionality. ## Configuration Example ```yaml theme={null} name: Delete Task key: delete-task type: delete-data-record config: dataSource: collectionKey: tasks # Required: record ID id: $var: $.input.taskId ``` ## Configuration Parameters * `dataSource` - Data source configuration * `collectionKey` - Identifier of the data collection to operate on * `collectionParameters` - Parameters passed to the collection methods * `id` - Record ID to delete (supports formulas) ## Output Schema ```yaml theme={null} type: object properties: id: type: string ``` # Find Data Record By ID Source: https://docs.getmembrane.com/docs/references/functions/function-types/find-data-record-by-id The `find-data-record-by-id` function type finds a record in a data collection by its ID. ## Usage This function type is used in Actions to provide access to data collection find by ID functionality. ## Configuration Example ```yaml theme={null} name: Find Task key: find-task type: find-data-record-by-id config: dataSource: collectionKey: tasks fieldMapping: importValue: title: $var: $.summary # Required: record ID id: $var: $.input.taskId ``` ## Configuration Parameters * `dataSource` - Data source configuration * `collectionKey` - Identifier of the data collection to operate on * `collectionParameters` - Parameters passed to the collection methods * `udm` - (for universal integrations only) Universal Data Model to read * `fieldMapping` - Field mapping configuration * `importValue` - Maps fields from external app to your app format * `id` - Record ID to find (supports formulas) ## Output Single record object or `null` if not found # GraphQL API Mapping Source: https://docs.getmembrane.com/docs/references/functions/function-types/graphql-api-mapping The GraphQL API Mapping function type maps functions to GraphQL API endpoints. It is defined like this: ```yaml theme={null} type: graphql-api-mapping mapping: query: | query GetContacts($limit: Int, $cursor: String) { contacts(first: $limit, after: $cursor) { edges { node { id firstName lastName email } } pageInfo { hasNextPage endCursor } } } variables: limit: 100 cursor: $var: $.cursor responseMapping: records: $var: response.data.contacts.edges[*].node ``` ## Format * `query` – The GraphQL query or mutation string * `variables` – Variables mapping for the GraphQL query * `responseMapping` – Mapping to transform the GraphQL response ## Variables * `credentials` – Authentication credentials from the current connection * `parameters` – Function-level and collection-level parameters combined * Function-specific input variables For `responseMapping`: * `response.data` – GraphQL response data * `response.errors` – GraphQL response errors (if any) * `response.extensions` – GraphQL response extensions (if any) ## Using in Connector Files When used in connector files, the mapping is written in a separate file named `.graphql.yml`. For example: ```yaml theme={null} # "list" function of the "Contacts" data collection. # File location: data/contacts/list.graphql.yml query: | query GetContacts($limit: Int, $cursor: String) { contacts(first: $limit, after: $cursor) { edges { node { id firstName lastName email } } pageInfo { hasNextPage endCursor } } } variables: limit: 100 cursor: $var: $.cursor responseMapping: records: $var: response.data.contacts.edges[*].node cursor: $cond: - $var: response.data.contacts.pageInfo.hasNextPage - $var: response.data.contacts.pageInfo.endCursor - null ``` # HTTP Request Source: https://docs.getmembrane.com/docs/references/functions/function-types/http-request The `http-request` function type makes arbitrary HTTP requests to any endpoint. This function type can be used in both Actions and Flow Nodes. ## Usage * **Actions** - Use `http-request` action type to make HTTP requests to any external endpoint * **Flow Nodes** - Use `custom-http-request` node type to make HTTP requests within flows ## Examples ### Action Implementation ```yaml theme={null} name: Call External API key: call-external-api type: http-request config: request: uri: $var: $.input.endpoint method: GET headers: Authorization: $formula: "Bearer " + $.input.apiKey Content-Type: application/json query: limit: 100 allowNon2xx: false ``` ### Flow Node Implementation ```yaml theme={null} name: Call External API type: custom-http-request config: url: $var: input.apiEndpoint method: POST headers: Authorization: $concat: - 'Bearer ' - $var: input.apiToken Content-Type: application/json body: data: $var: input.payload timeout: 30000 ``` ## Configuration Parameters ### Action Parameters * `request.uri` - Request URI (required) * `request.method` - HTTP method (defaults to GET) * `request.body` - Request body * `request.query` - Query parameters * `request.headers` - HTTP headers * `responseSchema` - Expected response schema (optional) * `allowNon2xx` - Allow non-200 responses (optional) ### Flow Node Parameters * `url` - Request URL (supports formulas) * `method` - HTTP method (GET, POST, PUT, DELETE, etc.) * `headers` - HTTP headers (supports formulas) * `body` - Request body (supports formulas) * `query` - Query parameters (supports formulas) * `timeout` - Request timeout in milliseconds (optional) ## Batch Mode In Flow Nodes, you can enable batch mode to avoid sending dozens or hundreds of HTTP requests. When using Batch Mode, the node's input becomes a list of up to `batchSize` items. # JavaScript Source: https://docs.getmembrane.com/docs/references/functions/function-types/javascript The JavaScript function type allows you to run arbitrary JavaScript or TypeScript code. It is defined like this: ```yaml theme={null} type: javascript code: | module.exports = async function({ input, externalApiClient }) { const response = await externalApiClient.post('/tasks', input) return response.id } ``` The code should always export a single function as the default export. ## Arguments Arguments are all passed as a single object to the function exported from the code. Additionally to function-specific arguments, the following arguments are added to all functions: * `externalApiClient` – API client configured with authentication that can be used to make requests to the external API. This parameter is available when code is executed in a context of a specific [Connection](/reference/workspace-elements/connections). * `credentials` – authentication credentials for the current connection (only available when externalApiClient is available) * `internalApiClient` - API client configured to make requests to your product's API (also called [Internal API](/docs/how-membrane-works/internal-api)). This client is only available in the context of [App Data Schemas](/reference/workspace-elements/app-data-schemas) at the moment. * `engineApiClient` – API client (MembraneClient instance) for interacting with the Membrane Engine API. ## Logging You can log messages to the console using the `console.log` function: ```javascript theme={null} console.log('Fetching items...') ``` To log a JSON object, you need to stringify it: ```javascript theme={null} console.log(JSON.stringify(data)) ``` ## API Clients JavaScript method implementations receive API client(s) named `externalApiClient`, `internalApiClient`, and `engineApiClient` as arguments. They can be used to make API requests to an external API (based on the current integration or connection), internal API (API of your product), and the Membrane Engine API respectively. The API clients are automatically configured with authentication and base URL settings - you don't need to provide authentication parameters into each request. ### Making Requests The API client provides methods for making HTTP requests: ```typescript theme={null} interface ApiClient { // GET request with optional query parameters get(path: string, query?: Record, options?: RestApiClientOptions): Promise // POST request with data post(path: string, data?: any, options?: RestApiClientOptions): Promise // PUT request with data put(path: string, data?: any, options?: RestApiClientOptions): Promise // PATCH request with data patch(path: string, data?: any, options?: RestApiClientOptions): Promise // DELETE request with optional data delete(path: string, data?: any, options?: RestApiClientOptions): Promise } ``` ### Examples ```javascript theme={null} // GET request with query parameters const response = await apiClient.get('/items', { page: 1, limit: 10, }) // POST request with data const newItem = await apiClient.post('/items', { name: 'New Item', description: 'Description', }) // PUT request to update await apiClient.put('/items/123', { name: 'Updated Item', }) // DELETE request await apiClient.delete('/items/123') ``` ### Request Options Each request method accepts an optional `options` parameter that can override the default client configuration: ```typescript theme={null} interface RestApiClientOptions { // Base URI for relative paths baseUri?: string // Headers to add to the request headers?: Record // Query parameters to add to all requests query?: Record // Basic auth credentials auth?: { username: string password: string } // Return full response instead of just data returnFullResponse?: boolean // Response type - how to parse the response responseType?: 'json' | 'arraybuffer' | 'stream' } ``` ### Example with Options ```javascript theme={null} // Get JSON response (default) const jsonResponse = await apiClient.get( '/items', {}, { headers: { Accept: 'application/json', }, }, ) // Download file as stream const fileResponse = await apiClient.get( '/files/123', {}, { responseType: 'stream', returnFullResponse: true, }, ) ``` ### Response By default, the API client: 1. Returns the response body for successful requests (status 200–299) 2. Throws an error for failed requests 3. Handles authentication and rate limiting errors automatically You can get the full response including status and headers by setting `returnFullResponse: true` in the options. ## Using in Actions and Flow Nodes You can use this function in actions or flow nodes with `run-javascript` type. It is structured a bit differently than the standard definition. Here is an example: ```yaml theme={null} name: Create Task key: create-task type: run-javascript config: code: | module.exports = async function({ input, externalApiClient }) { const response = await externalApiClient.post('/tasks', input) return response.id } ``` ### Error Handling The API client automatically handles common error cases: * 401 responses trigger an ACCESS\_TOKEN\_EXPIRED error that will attempt to refresh credentials * 429 responses trigger a RATE\_LIMIT\_EXCEEDED error that will retry the request with exponential backoff (only in asynchronous mode, e.g., within a flow run) * Other error responses (status >= 300) throw a ConnectionError ## Using in Connector Files When used in connector functionality that is stored in connector files (i.e. everything except functions in the connector definition itself), the code is written in a separate file named `.js`. For example: ```javascript theme={null} // "Create" function of the "Contacts" data collection inside a connector. // File location: data/contacts/create.js module.exports = async function ({ parameters, externalApiClient, input }) { const response = await externalApiClient.post('/contacts', input) return { id: response.id, } } ``` # List Data Records Source: https://docs.getmembrane.com/docs/references/functions/function-types/list-data-records The `list-data-records` function type returns a list of all data records in a collection, page by page. ## Usage This function type is used in Actions to provide access to data collection list functionality. ## Configuration Example ```yaml theme={null} name: List Tasks key: list-tasks type: list-data-records config: dataSource: collectionKey: tasks fieldMapping: importValue: title: $var: $.summary createdAt: $var: $.created # Optional: filtering and pagination filter: status: open unifiedFilter: assigneeId: $var: $.input.userId cursor: $var: $.input.cursor ``` ## Configuration Parameters * `dataSource` - Data source configuration * `collectionKey` - Identifier of the data collection to operate on * `collectionParameters` - Parameters passed to the collection methods * `udm` - (for universal integrations only) Universal Data Model to read * `fieldMapping` - Field mapping configuration * `importValue` - Maps fields from external app to your app format * `filter` - Filter criteria (values for specific fields that should be matched to return a record) * `unifiedFilter` - Unified query fields for filtering * `cursor` - Pagination cursor for next page ## Output Schema ```yaml theme={null} type: object properties: cursor: type: string records: type: array items: type: object # Based on field mapping configuration ``` # Mapping Source: https://docs.getmembrane.com/docs/references/functions/function-types/mapping The Mapping function type transforms input data into output data using [formulas](/docs/references/formulas). It is defined like this: ```yaml theme={null} type: mapping mapping: id: $var: input.record.id fullName: $concat: - $var: input.record.firstName - ' ' - $var: input.record.lastName ``` The mapping is a YAML object where each key is an output field and the value is a [formula](/docs/references/formulas) expression. ## Variables * `credentials` – Authentication credentials from the current connection * `parameters` – Function-level and collection-level parameters combined * Function-specific input variables ## Using in Actions and Flow Nodes You can use this function in flow nodes with `transform-data` type. It is structured a bit differently than the standard definition. Here is an example: ```yaml theme={null} type: transform-data config: output: outputField: $var: input.sourceField ``` ## Using in Connector Files When used in connector files, the mapping is written in a separate file named `.map.yml` (or `.yml`). For example: ```yaml theme={null} # "record-from-api" function of the "Contacts" data collection. # File location: data/contacts/record-from-api.map.yml id: $var: $.fields.id createdAt: $var: $.fields.created_at ``` # Match Data Record Source: https://docs.getmembrane.com/docs/references/functions/function-types/match-data-record The `match-data-record` function type finds a data record with fields matching a query. Unlike `find-data-record-by-id` which searches by ID, this function type searches by field values. ## Usage This function type is used in Actions to provide access to data collection match functionality. ## Configuration Example ```yaml theme={null} name: Find Contact by Email key: find-contact-by-email inputSchema: type: object properties: email: type: string type: match-data-record config: dataSource: collectionKey: contacts lookup: query: email: $var: $.input.email fieldMapping: importValue: name: $var: $.fullName email: $var: $.primaryEmail ``` ## Configuration Parameters * `dataSource` - Data source configuration * `collectionKey` - Identifier of the data collection to operate on * `collectionParameters` - Parameters passed to the collection methods * `udm` - (for universal integrations only) Universal Data Model to read * `lookup` - Query configuration for matching records * `query` - Field values to match against (supports formulas) * `universalQuery` - Universal field values for matching (for universal integrations) * `fieldMapping` - Field mapping configuration * `importValue` - Maps fields from external app to your app format ## Output Single record object or `null` if no match is found # REST API Mapping Source: https://docs.getmembrane.com/docs/references/functions/function-types/rest-api-mapping The REST API Mapping function type maps functions to REST API endpoints. It is defined like this: ```yaml theme={null} type: rest-api-mapping mapping: path: /contacts method: GET requestMapping: query: limit: 100 offset: $var: $.cursor responseMapping: records: $var: response.data.contacts ``` ## Format * `path` – The API endpoint path * `method` – HTTP method (GET, POST, PUT, PATCH, DELETE) * `requestMapping` – Request mapping * `query` – Query parameters mapping * `headers` – Headers mapping * `data` – Request body mapping for POST/PUT/PATCH * `pathParameters` – Values for placeholders in the path (e.g., `{projectId}`) * `responseMapping` – Mapping to transform the API response ## Variables * `credentials` – Authentication credentials from the current connection * `parameters` – Function-level and collection-level parameters combined * Function-specific input variables For `responseMapping`: * `response.data` – API response data * `response.headers` – API response headers * `response.statusCode` – API response status code ## Using in Connector Files When used in connector files, the mapping is written in a separate file named `.rest.yml` (or `.api.yml`). For example: ```yaml theme={null} # "list" function of the "Contacts" data collection. # File location: data/contacts/list.rest.yml path: /contacts method: GET requestMapping: query: limit: 100 offset: $var: $.cursor responseMapping: records: $var: response.data.contacts cursor: $var: response.data.nextOffset ``` # Run Action Source: https://docs.getmembrane.com/docs/references/functions/function-types/run-action The `run-action` function type executes predefined actions within flows. ## Usage This function type is used in Flow Nodes to run existing actions, allowing you to reuse action logic within flow execution. ## Flow Node Configuration Example ```yaml theme={null} name: Execute Create Contact Action type: run-action config: action: key: create-contact input: email: $var: input.email firstName: $var: input.firstName lastName: $var: input.lastName ``` ## Configuration Parameters * `action` * `key` - the key of the action to execute * `input` - Input data to pass to the action (supports formulas) ## Output The output matches the output schema of the executed action. # Search Data Record Source: https://docs.getmembrane.com/docs/references/functions/function-types/search-data-record The `search-data-record` function type searches records in a data collection using a text query. ## Usage This function type is used in Actions to provide access to data collection search functionality. ## Configuration Example ```yaml theme={null} name: Search Tasks key: search-tasks type: search-data-record config: dataSource: collectionKey: tasks fieldMapping: importValue: title: $var: $.summary # Required: search query query: $var: $.input.searchText cursor: $var: $.input.pageToken ``` ## Configuration Parameters * `dataSource` - Data source configuration * `collectionKey` - Identifier of the data collection to operate on * `collectionParameters` - Parameters passed to the collection methods * `udm` - (for universal integrations only) Universal Data Model to read * `fieldMapping` - Field mapping configuration * `importValue` - Maps fields from external app to your app format * `query` - Search query string * `cursor` - Pagination cursor ## Output Schema ```yaml theme={null} type: object properties: cursor: type: string records: type: array items: type: object ``` # Update Data Record Source: https://docs.getmembrane.com/docs/references/functions/function-types/update-data-record The `update-data-record` function type updates an existing data record by ID. ## Usage This function type is used in Actions to provide access to data collection update functionality. ## Configuration Example ```yaml theme={null} name: Update Task key: update-task inputSchema: type: object properties: title: type: string status: type: string type: update-data-record config: dataSource: collectionKey: tasks fieldMapping: exportValue: summary: $var: $.input.title status: $var: $.input.status # Required: record ID id: $var: $.input.taskId ``` ## Configuration Parameters * `dataSource` - Data source configuration * `collectionKey` - Identifier of the data collection to operate on * `collectionParameters` - Parameters passed to the collection methods * `udm` - (for universal integrations only) Universal Data Model to write * `fieldMapping` - Field mapping configuration * `exportValue` - Maps fields from your app to the external app format * `id` - Record ID to update (supports formulas) ## Output Schema ```yaml theme={null} type: object properties: id: type: string ``` # SDKs Source: https://docs.getmembrane.com/docs/references/sdks Membrane provides SDKs for building integrations in your preferred language and framework. | SDK | Package | Description | | - | - | - | | [TypeScript / JavaScript](/docs/ways-to-use-membrane/sdk) | `@membranehq/sdk` | Core SDK with typed accessors for all Membrane resources | | [React](/docs/ways-to-use-membrane/sdk/react) | `@membranehq/react` | React hooks and pre-built UI components | | [Vue.js](/docs/ways-to-use-membrane/sdk/vue) | `@membranehq/react` (adapter) | Vue composables wrapping the core SDK | | [CLI](/docs/ways-to-use-membrane/cli) | `@membranehq/cli` | Command-line interface for managing integrations | ## Install ```bash theme={null} # Core SDK npm install @membranehq/sdk # React SDK (includes core SDK) npm install @membranehq/react # CLI npm install -g @membranehq/cli ``` ## See Also * [REST API](/docs/ways-to-use-membrane/rest-api) — direct API access without an SDK * [MCP Server](/docs/ways-to-use-membrane/mcp) — expose Membrane as tools for AI agents * [CSP Rules](/docs/ways-to-use-membrane/sdk/csp-rules) — Content Security Policy configuration for browser usage # Universal Data Models Source: https://docs.getmembrane.com/docs/references/universal-data-models-ref Universal Data Models (UDM) let you work with data from third-party applications in a standardized way—without knowing anything about the underlying data structure. Connectors provide mappings from and to UDM for relevant data models. You can find currently supported Universal Data Models below. *** ## Activities **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | title | string | | | description | string | | | type | string | | | durationSeconds | number | | | location | string | | | isRecurrent | boolean | | | participants | list | item: object | | userId | string | | | contactId | string | | | status | string | | | companyId | string | | | dealId | string | | | leadId | string | | | ownerId | string | | | startTime | string | Format: date-time | | endTime | string | Format: date-time | | dueTime | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Bills **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | number | string | | | name | string | | | memo | string | | | status | string | | | paymentMethod | string | | | supplierId | string | | | currency | string | | | payments | list | item: string | | lineItems | list | item: object | |   id | string | | |   itemName | string | | |   description | string | | |   code | string | | |   type | string | | |   companyId | string | | |   ledgerAccountId | string | | |   quantity | number | | |   unitPrice | number | | |   unitOfMeasure | string | | |   purchasePrice | number | | |   salesPrice | number | | |   taxable | boolean | | |   taxAmount | number | | |   totalAmount | number | | | totalAmount | number | | | totalTax | number | | | exchangeRate | number | | | totalDiscount | number | | | subTotal | number | | | balance | number | | | ledgerAccountId | string | | | issueDate | string | Format: date-time | | dueDate | string | Format: date-time | | paidOnDate | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Campaign Members **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | fullName | string | | | firstName | string | | | lastName | string | | | primaryEmail | string | | | campaignId | string | | | contactId | string | | | leadId | string | | | source | string | | | status | string | | | companyId | string | | | jobTitle | string | | | hasOptedOutOfEmail | boolean | | | createdTime | string | Format: date-time | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Campaigns **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | cost | number | | | description | string | | | source | string | | | stage | string | | | type | string | | | ownerId | string | | | sentCount | number | Read Only | | replyCount | number | Read Only | | openCount | number | Read Only | | clickCount | number | Read Only | | startTime | string | Format: date-time | | endTime | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | | lastActivityTime | string | Format: date-time, Read Only | *** ## Companies **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | websiteUrl | string | | | phones | list | item: object | |   value | string | | |   type | string | | | primaryPhone | string | | | description | string | | | currency | string | | | industry | string | | | ownerId | string | | | primaryAddress | object | | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | addresses | list | item: object | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | numberOfEmployees | number | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | | lastActivityTime | string | Format: date-time, Read Only | *** ## Contacts **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | fullName | string | | | firstName | string | | | lastName | string | | | primaryEmail | string | | | emails | list | item: object | |   value | string | | |   type | string | | | primaryPhone | string | | | phones | list | item: object | |   value | string | | |   type | string | | | primaryAddress | object | | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | addresses | list | item: object | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | stage | string | | | companyName | string | | | companyId | string | | | ownerId | string | | | jobTitle | string | | | source | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | | lastActivityTime | string | Format: date-time, Read Only | *** ## Contact Lists **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | description | string | | | source | string | | | type | string | | | ownerId | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Credit Notes **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | number | string | | | memo | string | | | status | string | | | customerId | string | | | currency | string | | | exchangeRate | number | | | payments | list | item: string | | lineItems | list | item: object | |   id | string | | |   itemName | string | | |   description | string | | |   quantity | number | | |   totalLineAmount | number | | |   companyId | string | | |   ledgerAccountId | string | | |   unitPrice | number | | |   taxRate | string | | | allocations | list | item: object | |   id | string | | |   type | string | Enum: invoice, order, expense, credit\_memo, over\_payment, pre... | |   amount | number | | | totalAmount | number | | | totalTax | number | | | totalDiscount | number | | | subTotal | number | | | balance | number | | | ledgerAccountId | string | | | issueDate | string | Format: date-time | | paidOnDate | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Customers **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | code | string | | | description | string | | | firstName | string | | | lastName | string | | | companyName | string | | | companyId | string | | | status | any | | | currency | string | | | addresses | list | item: object | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | phones | list | item: object | |   value | string | | |   type | string | | | emails | list | item: object | |   value | string | | |   type | string | | | websiteUrl | string | | | notes | string | | | tax\_number | string | | | orderIds | list | item: string | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Deal Products **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | quantity | number | | | price | number | | | dealId | string | | | productId | string | | | isActive | boolean | | | createdTime | string | Format: date-time | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Deals **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | amount | number | | | currency | string | | | companyId | string | | | contactId | string | | | campaignId | string | | | source | string | | | description | string | | | stage | string | | | stageId | string | | | status | string | Enum: open, won, lost | | type | string | | | ownerId | string | | | probability | number | | | closeTime | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | | lastActivityTime | string | Format: date-time, Read Only | *** ## Deal Stages **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | state | string | Enum: open, won, lost | *** ## Documents **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | uri | string | Read Only | | title | string | | | parentDocumentId | string | | | canHaveChildren | boolean | Read Only | | isDownloadable | boolean | Default: true, Read Only | | text | string | | | ownerId | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Drives **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | description | string | | | ownerId | string | | | uri | string | Read Only | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Drive Items **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | itemType | string | Enum: file, folder, Allow Custom: true | | name | string | | | mimeType | string | | | description | string | | | previewUri | string | Read Only | | size | integer | Read Only | | path | string | Read Only | | folderId | string | | | ownerId | string | | | driveId | string | | | downloadUri | string | Read Only | | uri | string | Read Only | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Eeocs **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | | | candidateId | string | | | race | string | | | gender | string | | | veteranStatus | string | | | disabilityStatus | string | | | submittedTime | string | Format: date-time, Read Only | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Emails **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | threadId | string | Read Only | | to | list | item: string | | cc | list | item: string | | bcc | list | item: string | | from | string | | | replyTo | string | | | subject | string | | | body | string | | | html\_body | string | | | status | string | | | ownerId | string | | | contactId | string | | | leadId | string | | | companyId | string | | | dealId | string | | | attachments | list | item: object | |   id | string | | |   type | string | | |   name | string | | |   url | string | | |   content | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Employees **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | fullName | string | | | firstName | string | | | lastName | string | | | userName | string | | | preferredName | string | | | personalEmail | string | | | workEmail | string | | | mobileNumber | string | | | homeAddress | object | | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | workAddress | object | | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | employmentStatus | string | Enum: active, inactive, terminated, other | | companyId | string | | | departmentId | string | | | startDate | string | Format: date-time | | terminationDate | string | Format: date-time | | gender | string | Enum: male, female, other, not\_specified, Allow Custom: true | | ethnicity | string | | | birthday | string | Format: date-time | | photoUrl | string | | | timezone | string | | | title | string | | | employments | list | item: any | | compensations | list | item: object | |   id | string | | |   jobId | string | | |   rate | number | | |   paymentUnit | string | | |   currency | string | | |   paymentFrequency | string | | |   flsaStatus | string | | |   effectiveDate | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Files **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | mimeType | string | | | name | string | | | description | string | | | previewUri | string | Read Only | | size | integer | Read Only | | path | string | Read Only | | folderId | string | | | ownerId | string | | | driveId | string | | | downloadUri | string | Read Only | | uri | string | Read Only | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Folders **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | description | string | | | size | integer | Read Only | | path | string | Read Only | | parentFolderId | string | | | ownerId | string | | | driveId | string | | | downloadUri | string | Read Only | | uri | string | Read Only | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Invoices **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | number | string | | | name | string | | | memo | string | | | status | string | | | paymentMethod | string | | | customerId | string | | | currency | string | | | payments | list | item: string | | lineItems | list | item: object | |   id | string | | |   itemName | string | | |   description | string | | |   code | string | | |   type | string | | |   status | string | | |   companyId | string | | |   ledgerAccountId | string | | |   quantity | number | | |   unitOfMeasure | string | | |   unitPrice | number | | |   purchasePrice | number | | |   salesPrice | number | | |   taxable | boolean | | |   taxAmount | number | | |   totalAmount | number | | |   inventoryDate | string | Format: date-time | | totalAmount | number | | | totalTax | number | | | exchangeRate | number | | | totalDiscount | number | | | subTotal | number | | | balance | number | | | ledgerAccountId | string | | | issueDate | string | Format: date-time | | dueDate | string | Format: date-time | | paidOnDate | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Job Applications **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | | | candidateId | string | | | jobId | string | | | appliedAt | string | Format: date-time | | status | string | | | currentStage | string | | | source | string | | | rejectedTime | string | Format: date-time | | rejectionReason | string | | | creditedTo | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Job Candidates **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | | | fullName | string | | | firstName | string | | | lastName | string | | | company | string | | | title | string | | | addresses | list | item: object | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | phones | list | item: object | |   value | string | | |   type | string | | | primaryEmail | string | | | primaryPhone | string | | | emails | list | item: object | |   value | string | | |   type | string | | | urls | list | item: object | |   value | string | | |   type | string | | | applicationIds | list | item: string | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Job Interviews **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | | | candidateId | string | | | organizerId | string | | | interviewerIds | list | item: string | | stage | string | | | status | string | | | location | string | | | startTime | string | Format: date-time | | endTime | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Job Offers **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | | | jobApplicationId | string | | | status | string | | | sentTime | string | Format: date-time | | closedTime | string | Format: date-time | | startDate | string | Format: date | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Jobs **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | | | name | string | | | description | string | | | code | string | | | status | string | | | isConfidential | boolean | | | hiringManagersIds | list | item: string | | recruitersIds | list | item: string | | departmentsIds | list | item: string | | officesIds | list | item: string | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Journal Entries **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | number | string | | | title | string | | | memo | string | | | currency | string | | | companyId | string | | | lineItems | list | item: object | |   id | string | | |   description | string | | |   type | string | Enum: debit, credit | |   amount | number | | |   ledgerAccountId | string | | |   companyId | string | | |   contactId | string | | | exchangeRate | number | | | taxType | string | | | taxCode | string | | | ledgerAccountId | string | | | payments | list | item: string | | transactionDate | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | updatedTime | string | Format: date-time, Read Only | *** ## Leads **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | fullName | string | | | firstName | string | | | lastName | string | | | primaryEmail | string | | | emails | list | item: object | |   value | string | | |   type | string | | | primaryPhone | string | | | phones | list | item: object | |   value | string | | |   type | string | | | primaryAddress | object | | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | addresses | list | item: object | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | stage | string | | | value | number | | | companyName | string | | | companyId | string | | | ownerId | string | | | jobTitle | string | | | source | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | | lastActivityTime | string | Format: date-time, Read Only | *** ## Ledger Accounts **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | code | string | | | description | string | | | classification | string | Enum: Asset, Liability, Equity, Revenue, Expense | | type | string | | | status | string | Enum: Active, Inactive, Archived | | currentBalance | number | | | currency | string | | | taxRateId | string | | | companyId | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Meetings **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | title | string | | | description | string | | | durationSeconds | number | | | location | string | | | isRecurrent | boolean | | | participants | list | item: object | | userId | string | | | contactId | string | | | leadId | string | | | status | string | | | companyId | string | | | dealId | string | | | ownerId | string | | | startTime | string | Format: date-time | | endTime | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Messages **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | subject | string | | | channelId | string | | | threadId | string | | | text | string | | | ownerId | string | | | status | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Notes **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | title | string | | | content | string | | | status | string | | | ownerId | string | | | contactId | string | | | leadId | string | | | companyId | string | | | dealId | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Orders **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | notes | string | | | status | string | Enum: open, closed | | customerId | string | | | currency | string | | | totalAmount | number | | | paymentMethod | string | | | paymentStatus | string | | | fullFillmentStatus | string | | | billingAddress | object | | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | shippingAddress | object | | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | lineItems | list | item: object | |   name | string | | |   productId | string | | |   sku | string | | |   quantity | number | | |   unitPrice | number | | |   totalAmount | number | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Payments **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | status | string | | | type | string | | | notes | string | | | currency | string | | | exchangeRate | string | | | totalAmount | number | | | supplierId | string | | | customerId | string | | | ledgerAccountId | string | | | paymentMethod | string | | | transactionDate | string | Format: date-time | | isReconciled | boolean | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Payrolls **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | employeeId | string | | | companyId | string | | | runState | string | Enum: PAID, DRAFT, APPROVED, CLOSED, Allow Custom: true | | runType | string | Enum: REGULAR, OFF\_CYCLE, UNSCHEDULED, CORRECTION, TERMINATI..., Allow Custom: true | | isProcessed | boolean | | | processedTime | string | Format: date-time | | checkTime | string | Format: date-time | | startTime | string | Format: date-time | | endTime | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Products **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | description | string | | | code | string | | | type | string | | | weight | number | | | isActive | boolean | | | price | number | | | status | string | | | options | list | item: object | |   id | string | | |   name | string | | |   values | list | item: string | | variants | list | item: object | |   id | string | | |   name | string | | |   price | number | | |   sku | string | | |   weight | number | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Projects **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | description | string | | | type | string | | | parentId | string | | | ownerId | string | | | startTime | string | Format: date-time | | dueTime | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Suppliers **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | code | string | | | description | string | | | firstName | string | | | lastName | string | | | companyName | string | | | companyId | string | | | status | string | | | currency | string | | | addresses | list | item: object | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | phones | list | item: object | |   value | string | | |   type | string | | | emails | list | item: object | |   value | string | | |   type | string | | | websiteUrl | string | | | notes | string | | | taxNumber | string | | | orderIds | list | item: string | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Tasks **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | subject | string | | | content | string | | | status | string | | | type | string | | | priority | string | | | ownerId | string | | | assigneeId | string | | | followerIds | list | item: string | | parentTaskId | string | | | projectId | string | | | tags | list | item: string | | contactId | string | | | leadId | string | | | companyId | string | | | dealId | string | | | dueTime | string | Format: date-time | | startTime | string | Format: date-time | | endTime | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Tax Rates **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | name | string | | | code | string | | | description | string | | | effectiveTaxRate | number | | | totalTaxRate | number | | | type | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Users **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | title | string | | | fullName | string | | | firstName | string | | | lastName | string | | | userName | string | | | primaryEmail | string | | | emails | list | item: object | |   value | string | | |   type | string | | | primaryPhone | string | | | phones | list | item: object | |   value | string | | |   type | string | | | primaryAddress | object | | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | addresses | list | item: object | |   type | string | | |   full | string | | |   street | string | | |   city | string | | |   state | string | | |   country | string | | |   zip | string | | | companyName | string | | | companyId | string | | | imageUrl | string | | | timezone | string | | | isActive | boolean | | | roleId | string | | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Timeoff Balances **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | employeeId | string | | | balanceHours | number | | | usedHours | number | | | policyType | string | Enum: VACATION, SICK, PERSONAL, JURY\_DUTY, VOLUNTEER, BEREAVE..., Allow Custom: true | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** ## Timeoff Requests **Fields Schema** | Field | Type | Notes | | - | - | - | | id | string | Read Only | | employeeId | string | | | approverId | string | | | status | string | Enum: REQUESTED, APPROVED, DECLINED, CANCELLED, Allow Custom: true | | employeeNote | string | | | units | string | Enum: DAYS, HOURS, Allow Custom: true | | amount | number | | | requestType | string | Enum: VACATION, SICK, PERSONAL, JURY\_DUTY, VOLUNTEER, BEREAVE..., Allow Custom: true | | startTime | string | Format: date-time | | endTime | string | Format: date-time | | createdTime | string | Format: date-time, Read Only | | createdBy | string | Read Only | | updatedTime | string | Format: date-time, Read Only | | updatedBy | string | Read Only | *** *** > **Tip:**\ > Universal Data Models make it easy to build integrations that work across many apps, by providing a consistent schema for common business objects.\ > For more details on mapping and usage, see the [Connector Builder documentation](/reference/workspace-elements/connectors). *** # Support Source: https://docs.getmembrane.com/docs/support Get help with building and maintaining your integrations. Professional service for building integrations end-to-end with AI assistance. # Membrane Expert Source: https://docs.getmembrane.com/docs/support/membrane-expert Membrane Expert is a professional service for building integrations end-to-end with the help of AI. While Membrane Agent can implement a large portion of integration use cases autonomously, it makes mistakes. Normally you would need an integration expert on your team to make sure everything you need works end-to-end. Membrane Expert removes this requirement and serves as your in-house human expert on integrations. It helps you accomplish the following: * Implement an end-to-end prototype of your integration use case. * Build new connectors or modify existing ones. * Scale use case to more external APIs. * Troubleshoot / improve integration use cases. It works like this: Membrane Expert illustration 1. You submit a Membrane Expert request describing what you want to be built. 2. Our team member is assigned to coordinate your request. 3. AI guided by our team works in short iterations and sends you an update after each iteration. 4. You can provide feedback and/or change the direction of work at any time. Using Membrane Expert is like having a world-class AI-powered integration team working on your product. ## Getting Started > **Note:** Membrane Expert is only available to customers on our [Enterprise plan](https://getmembrane.com/pricing) . To get started with Membrane Expert, send us a message through one of the communication channels. If it is your first time using Membrane Expert, we will ask the approval of your billing admin and set up the monthly budget for Membrane Expert credits. We will let you know when this is done and we started work on your request. ## Iterations Membrane Expert works in short iterations, each taking from 1 hour to 3 days on average. Depending on your subscription plan, you may have different SLA for the average duration of an iteration and a number of parallel iterations. Each iteration is a session of AI-powered development work done by one of the members of our Solutions team. After each iteration, you will get an update with details on what was done and any additional information or decisions we need from you to continue. ## Pricing and Billing Each Membrane Expert Credit costs \$100. Each iteration costs from 1 to 5 credits based on the amount of work and compute resources used. We can give you a ballpark estimate of the cost of your request ahead of time, but never commit to a fixed cost due to high uncertainty and flexible nature of integration development. Used Membrane Expert credits will be added to your monthly usage invoice. # Agent Skills Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/agent-skills Agent Skills give AI agents that support skills (Claude Code, Cursor, Windsurf, Codex, and others) the ability to work with Membrane directly. ## Install Run in your project directory: ```bash theme={null} npx skills add membranehq/agent-skills ``` This gives your agent tools for managing connections, running actions, and working with data. The agent will set up everything it needs automatically (MCP server, credentials, etc.). ## Available Skills ### integrate-anything The core skill for connecting to external apps and working with them. Use this when you want your agent to connect to apps, run actions, subscribe to events, and manage integrations. ```bash theme={null} npx skills add membranehq/agent-skills --skill integrate-anything ``` See [Quickstart: AI Agents](/docs/getting-started/quickstart-agent) for getting started. ### integration-development For building customer-facing integration features into your product — integrations pages, connection UIs, data sync, and multi-tenant authentication. ```bash theme={null} npx skills add membranehq/agent-skills --skill integration-development ``` See [Quickstart: Product Integrations](/docs/getting-started/product-integrations) for getting started. ## Using Skills Once installed, prompt your agent naturally: > "Connect to HubSpot and create an action that syncs new contacts to our database." > > "Build an integrations page where my users can connect to external apps." The agent will use Membrane's tools to set up integrations, create connections, and implement the logic. ## Other Ways to Get Agents to Use Membrane * **[MCP Server](/docs/ways-to-use-membrane/mcp)** — for agents that support MCP but not skills (Claude Desktop, ChatGPT, etc.) * **[In-app AI Agent](/docs/getting-started/product-integrations/in-app-ai-agent)** — for embedding an AI assistant in your product that acts on behalf of each user # CLI Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/cli The Membrane CLI lets you manage integrations, run actions, and sync configuration from your terminal. ## Install ```bash theme={null} npm install -g @membranehq/cli ``` ## Authenticate ```bash theme={null} membrane login ``` ## Commands ### Setup Authenticate, check status, and navigate your workspace. | Command | Description | | - | - | | `membrane login` | Authenticate with Membrane | | `membrane logout` | Clear stored credentials | | `membrane status` | Show current workspace status | | `membrane open` | Open a resource in the browser (Console) | | `membrane search` | Search Membrane resources | ### Connect Create and manage connections to external apps. | Command | Description | | - | - | | `membrane connect` | Create a new connection to an external app | | `membrane connection` | Manage existing connections (list, get, ensure, archive) | | `membrane connection-request` | Manage connection requests (OAuth flows) | ### Act Run actions and make API requests through connections. | Command | Description | | - | - | | `membrane action` | Run or manage actions (list, get, create, run) | | `membrane request` | Make an HTTP request through a connection | ### Workspace Management Sync your workspace configuration between Membrane and local files. | Command | Description | | - | - | | `membrane pull` | Pull configuration from Membrane to local files | | `membrane push` | Push local configuration to Membrane | | `membrane diff` | Show differences between local and remote | # Console Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/console The [Membrane Console](https://console.getmembrane.com) is the web interface for managing your integrations, testing them, troubleshooting issues, and monitoring everything that happens. ## Workspaces Workspaces are the unit of organization in Membrane. You can create new workspaces for different environments, to group your apps and agents, or for any other purpose. You can easily migrate between workspaces with [Import & Export](/docs/managing-membrane/import-and-export). ## Your Software Manage the apps and agents that use Membrane. When adding a new agent or app, you get an interactive guide to get it started. ## Your Connections Create new connections to external apps, manage existing ones, monitor their status, and troubleshoot issues. See [Connections](/docs/how-membrane-works/connections) for details. ## Tenants If multiple people or groups of people use Membrane, Tenants create an isolation boundary between them within the same workspace. By default, each user gets their own tenant, but you can create them for any purpose whenever you want separate groups of connections and integration settings. See [Tenants](/docs/how-membrane-works/tenants) for details. ## Exploring Membrane Elements In the Explore section, you can find all the elements in your workspace — actions, flows, data sources, field mappings, and more. Browse, search, and inspect the configuration of any element. ## Activity Log Monitor everything that happens between your software and external apps — API requests, webhooks, events, flow runs, and errors. See [Monitoring & Troubleshooting](/docs/managing-membrane/monitoring-troubleshooting) for details. # Embedded UI Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/embedded-ui Membrane provides a pre-built Connect UI for creating and managing connections inside your app. It lives in the [JavaScript SDK](/docs/ways-to-use-membrane/sdk). ## Install ```bash theme={null} npm install @membranehq/sdk ``` ## Connect UI Use `membrane.ui.connect()` to open the connection flow. This is the main way to let users connect to external apps from your product. ```javascript theme={null} const connection = await membrane.ui.connect({ integrationKey: 'hubspot' }) // Returns the connection object, or null if user cancels ``` You can also connect by intent — Membrane will find the right app automatically: ```javascript theme={null} const connection = await membrane.ui.connect({ intent: 'Connect to a Custom App (https://custo.mydomain.com)' }) ``` ### Options | Option | Description | | - | - | | `intent` | Describe what you want to connect to — Membrane finds the right app | | `integrationKey` | Connect to a specific integration by key | | `integrationId` | Connect to a specific integration by ID | | `connectorId` | Connect using a specific connector (without integration) | | `connectorKey` | Connect using a specific connector by key (without integration) | | `externalAppId` | Connect using a specific external app by ID | | `externalAppKey` | Connect using a specific external app by key | | `connectionId` | Reconnect or update an existing connection | | `name` | Pre-set a custom name for the connection | | `connectorParameters` | Custom connector parameters (e.g., OAuth scopes) | | `authOptionKey` | Select a specific authentication method | | `allowMultipleConnections` | Always create a new connection instead of reusing the existing one | | `theme` | `'light'`, `'dark'`, or `'auto'` (default: auto-detect from CSS) | ## Existing Connection UI Use `membrane.ui.connection()` to open the management UI for an existing connection — shows status, allows reconnection. ```javascript theme={null} await membrane.ui.connection({ connectionId: 'con_abc123' }) ``` ## Building a Full Connection Management Experience For end-to-end connection management in your app (listing, connecting, disconnecting, reconnecting), see [Connection UI](/docs/ways-to-use-membrane/embedded-ui/connection-ui) and the [Integration Catalog](/docs/getting-started/product-integrations/integration-catalog) guide. # Connection UI Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/embedded-ui/connection-ui The default connection UI (`membrane.ui.connect()`) handles the full authentication flow automatically. See [Embedded UI](/docs/ways-to-use-membrane/embedded-ui) for basic usage. This section covers advanced customization: Build a fully custom connection experience that blends into your app's design. Use HTML forms or REST API directly instead of the JavaScript SDK. Pass custom OAuth scopes, API versions, or other connector-specific parameters. # Connection UI Without the Front-End SDK Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/embedded-ui/connection-ui/connection-ui-without-the-front-end-sdk # Creating a Connection Without the Front-End SDK > This guide explains how to create an integration connection in Membrane **without** using the front-end SDK. This is useful for backend-to-backend integrations, custom UIs, or automation scripts.\ > For the recommended process of creating a connection, check [Connection UI](/docs/ways-to-use-membrane/embedded-ui/connection-ui/custom-connection-ui). ## Overview The connection process is initiated by making a request to the `/connect` endpoint of the API.\ This endpoint handles all authentication flows (OAuth2, OAuth1, client credentials, proxy) and manages the connection lifecycle. After the connection process is completed (successfully or not), the user will be redirected to the `redirectUri` you specify in the parameters. Opening the `/connect` endpoint should happen in your user's browser since many authentication flows require user interaction. **High-level flow:** 1. Your backend (or custom UI) redirects the user to `/connect` with the required parameters. 2. The user is redirected (if needed) to the OAuth provider for authentication. 3. After authentication, Membrane redirects the user (or the flow) to your specified `redirectUri` with the result. 4. Your backend receives the result and can proceed accordingly. *** ## Constructing the Request ### Endpoint ```http theme={null} POST /connect Content-Type: application/json ``` ### Parameters You can send parameters either as query parameters or in a JSON-stringified `payload` parameter in the body of a POST request (recommended). * `integrationId` or `integrationKey`: The integration to connect. * `connectorId` or `connectorKey`: A connector to connect directly (no integration required). * `externalAppId` or `externalAppKey`: An external app to connect directly. * `input`: Object with connection-specific input (may be empty for some integrations). If passed through query, this value should be JSON-stringified. * `connectionId`: If updating an existing connection. * `name`: Name for the connection. * `authOptionKey`: For integrations with multiple auth options. * `allowMultipleConnections`: if not set to `true`, only one connection per target (integration, connector, or external app) and tenant is allowed. New connections will replace existing ones. * `customState`: Custom string to persist through the flow. * `token`: JWT that authenticates the tenant workspace initiating the connection (See [Authentication](/docs/how-membrane-works/authentication)). * `redirectUri`: **URL to which the result will be sent (your backend endpoint).** ### Example Request This is an example HTML form that submits a correctly formatted connection request. ```html theme={null}
``` ```curl --location 'https://api.getmembrane.com/connect?integrationId=your-integration-id' \ theme={null} --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Authorization: ••••••' \ --data-urlencode 'payload={ "input":{"apiKey":"user-api-key"}, "authOptionKey": "", "allowMultipleConnections": false, "redirectUri":"https://your-backend.com/integration/callback" }' ``` ### Parameters Schema To understand which parameters are required to create a connection, see the [Custom Connection UI](/docs/ways-to-use-membrane/embedded-ui/connection-ui/custom-connection-ui). ## Connection Flow * If the integration uses OAuth, the user will be redirected to the provider for authentication. * After authentication, Membrane will redirect the user to the `redirectUri` you provided. * If the integration uses client credentials or similar, the connection may be created immediately and the result sent to your `redirectUri`. ## Handling the Callback Your backend should expose an endpoint matching the `redirectUri` you provided. Membrane will redirect to this URL with the following query parameters: ### On Success * `connectionId`: The ID of the newly created or updated connection. **Example:** ``` GET https://your-backend.com/integration/callback?connectionId=abc123 ``` ### On Error * `error`: Error message. * `errorData`: JSON-encoded error details. **Example:** ``` GET https://your-backend.com/integration/callback?error=OAuth+failed&errorData=%7B...%7D ``` # Custom Connection UI Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/embedded-ui/connection-ui/custom-connection-ui You can open the prebuilt connection UI for a specific integration with: ```javascript theme={null} await membrane.ui.connect({ integrationKey: 'hubspot' }) ```
This guide shows you how you can implement a custom connection UI ## 1. Get Auth Options To get information about what to ask the user in order to create a connection, fetch connection options: ```javascript theme={null} const integration = await membrane.integration('hubspot').get() // Fetch connection options const connectionOptions = await membrane.post('/connection-options', { integrationId: integration.id, }) ```
For simplicity, we'll assume there is only one auth option. If there are more, you can use the same logic for each of them. If there are zero options, it means authentication is not configured and you need to edit or change the connector. Each connection option contains: * `key` – Unique identifier for this auth option (pass to `authOptionKey` when connecting) * `type` – The authentication type (e.g., 'oauth2', 'client-credentials') * `title` – Human-readable name for this auth option * `description` – Description explaining this auth method * `inputSchema` – [Data Schema](/docs/references/data-schemas) of the parameters required to create a connection. It will be empty if parameters are not required. ## 2. Connection Parameters UI You can use your preferred way of converting [JSON Schema](https://json-schema.org/) into a form. For example, you can extract a list of fields from the schema and simply display an input for each: ```javascript theme={null} function ConnectionForm() { const [input, setInput] = useState({}) const schema = { type: 'object', properties: { email: { type: 'string' }, apiKey: { type: 'string' }, }, } const fields = Object.keys(schema.properties) return ( <> {fields.map((field) => (
setInput({ ...input, [field]: e.target.value })} />
))} ) } ```
If a connection doesn't have parameters, you can skip this step. ## 3. Creating a Connection When you have collected connection input, you can create a connection: ```javascript theme={null} const connection = await membrane.integration('hubspot').connect({ input }) ```
> This code may open a new window if it is required by the authentication process. > Make sure this code runs inside a user action handler (e.g. `onClick`), otherwise the browser may block the new window.
**Redirect instead of a new window** When you need to avoid opening a new window, you can use redirect instead. Pass `sameWindow: true` and `redirectUri` options. The URI will be used for redirection after the connection is created or fails. Added query parameters: * For successful creation, the `connectionId` will be added * For failures, `error` will contain the error message and `errorData` will contain the JSON stringified error payload ```javascript theme={null} const connection = await membrane.integration('hubspot').connect({ input, sameWindow: true, redirectUri: window.location.href, }) ```
## 4. Putting it all together Here is a simple UI component that displays the connection parameters and creates a new connection: ```javascript theme={null} import { useState, useEffect } from 'react' import { useIntegration, useMembrane } from '@membranehq/react' function Component({ integrationKey }) { // For this to work, wrap this component in const membrane = useMembrane() const { integration, error } = useIntegration(integrationKey) const [connectionOptions, setConnectionOptions] = useState(null) const [connectionInput, setConnectionInput] = useState({}) const [connecting, setConnecting] = useState(false) const [connection, setConnection] = useState(null) // Fetch connection options useEffect(() => { if (!integration?.id) return membrane .post('/connection-options', { integrationId: integration.id, }) .then(setConnectionOptions) }, [integration?.id, membrane]) // If something bad happened - show the error if (error) { return

Error: {error.message}

} // Wait until the spec and connection options load if (!integration || !connectionOptions) { return

Loading...

} // Display only the first option const option = connectionOptions[0] if (!option) { return

No auth options found for this integration

} // Get connection input schema const schema = option.inputSchema // Simplified way to get the list of connection parameters. const fields = schema ? Object.keys(schema.properties ?? {}) : [] async function connect() { setConnecting(true) try { const connection = await membrane.integration(integrationKey).connect({ input: connectionInput, authOptionKey: option.key, }) setConnection(connection) } finally { setConnecting(false) } } return (
{fields.length > 0 && (
Connection Parameters: {fields.map((field) => (
setConnectionInput({ ...connectionInput, [field]: e.target.value, }) } />
))}
)} {connecting ? Connecting... : } {connection && (
Connected!
Connection Id: {connection.id}
)}
) } ```
## 5. Re-connecting When a connection becomes disconnected, you can re-connect it using the same code as when creating a new connection: ```javascript theme={null} const connection = await membrane.integration('hubspot').connect({ input }) ```
## Multiple Connections for the same target By default, a `connect(...)` call reuses the existing connection for the same tenant when one exists. This applies whether you target an integration (`integrationId` / `integrationKey`), a connector (`connectorId` / `connectorKey`), or an external app (`externalAppId` / `externalAppKey`). Set `allowMultipleConnections: true` on the `connect` method call to always create a new connection instead of reusing an existing one. # Custom Connector Parameters Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/embedded-ui/connection-ui/custom-connector-parameters Custom connector parameters allow you to override or extend the integration's default connector configuration on a per-connection basis. These are developer-provided values that customize how the connector behaves for specific connections. ## Use Cases Common use cases for custom connector parameters include: * **Custom OAuth scopes** for specific connection needs * **Dynamic environment selection** if you want to programmatically decide whether to use sandbox/prod environment without asking user. * **Having different versions of connector behavior** for different connections that don't require user input. ## Important Notes * Custom connector parameters are **developer-provided**, not user-editable fields * They are stored with the connection and used for all API calls through that connection * They take precedence over the integration's default connector parameters * They are optional - connections work without them using default integration settings *** ## Method 1: Using ui.connect() When you want to open the connection UI with custom connector parameters: ```javascript theme={null} await membrane.ui.connect({ integrationKey: 'hubspot', allowMultipleConnections: true, connectorParameters: { scopes: ['contacts', 'deals', 'companies'], // Custom OAuth scopes }, }) ```
## Method 2: Using connect() For programmatic connection creation with custom parameters: ```javascript theme={null} const connection = await membrane.integration('hubspot').connect({ input: { // User-provided connection input apiKey: 'user-api-key', }, connectorParameters: { // Developer-provided connector customizations scopes: ['contacts', 'deals', 'companies'], }, }) ```
## Method 3: Backend connection (no front-end SDK) For backend-to-backend integrations or when not using the frontend SDK: ### HTML Form Example ```html theme={null}
``` # MCP Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/mcp Membrane provides a cloud MCP server that exposes its API as tools for AI agents. Your agent gets access to all Membrane operations — managing connections, running actions, querying data, and more. ## Setup Add Membrane's MCP server to your agent's configuration: ``` https://api.getmembrane.com/mcp/integrate-anything ``` **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`): ```json theme={null} { "mcpServers": { "membrane": { "type": "url", "url": "https://api.getmembrane.com/mcp/integrate-anything" } } } ``` **Cursor** (`.cursor/mcp.json` in your project): ```json theme={null} { "mcpServers": { "membrane": { "type": "url", "url": "https://api.getmembrane.com/mcp/integrate-anything" } } } ``` For other MCP clients, use the URL above and follow your client's configuration format. ## Authentication The MCP server supports OAuth — your agent will prompt you to log in when it first connects. If your MCP client doesn't support OAuth, you can authenticate by adding a Bearer token in the `Authorization` header: ``` Authorization: Bearer eyJhbGciOiJIUzUxMiIs... ``` As a last resort, append the token to the URL: ``` https://api.getmembrane.com/mcp/integrate-anything?token=eyJhbGciOiJIUzUxMiIs... ``` See [Authentication](/docs/how-membrane-works/authentication) for how to get a token. # REST API Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/rest-api Use the REST API to access all Membrane operations — connections, actions, events, data, and more. ## Making API Requests To make API requests, you need a Membrane Token. You can generate in your code or in \[Console]. See [Authentication](/docs/how-membrane-works/authentication) for more details. ```bash theme={null} curl https://api.getmembrane.com/self \ -H "Authorization: Bearer YOUR_TOKEN" ``` See [API Reference](/reference) for the full list of available API endpoints and their inputs/outputs. ## OpenAPI Spec The full OpenAPI specification is available at: ``` https://api.getmembrane.com/-json ``` ## API Versioning The API uses `X.Y` versioning. The major version changes when there are breaking changes, and the minor version changes with any updates. Include the `API-Version` header to pin a specific version: ```bash theme={null} curl https://api.getmembrane.com/self \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "API-Version: 2.1" ``` See the [Changelog](https://changelog.getmembrane.com) for version history and changes. # JavaScript SDK Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/sdk The Membrane JavaScript SDK provides typed accessors for all Membrane resources. It works in both Node.js and browser environments. ## Install ```bash theme={null} npm install @membranehq/sdk ``` ## Initialize ```javascript theme={null} import { MembraneClient } from '@membranehq/sdk' // With a static token const membrane = new MembraneClient({ token: process.env.MEMBRANE_ACCESS_TOKEN, }) // With dynamic token fetching — for long-lived environments (like web pages) // where the token may expire and needs to be refreshed const membrane = new MembraneClient({ fetchToken: async () => { const response = await fetch('/api/membrane-token') const { token } = await response.json() return token }, }) ``` See [Authentication](/docs/how-membrane-works/authentication) for how to get an access token. ## Usage ```javascript theme={null} // List integrations const integrations = await membrane.integrations.find() // Get a specific connection const connection = await membrane.connection('hubspot').get() // Run an action const result = await membrane.action('create-contact').run({ email: 'jane@example.com', name: 'Jane', }) // Open connection UI await membrane.ui.connect({ integrationKey: 'salesforce' }) ``` ## SDK Reference See the [SDK Reference](https://console.getmembrane.com/ref/sdk/index.html) for the full list of accessors, methods, and types. # CSP Rules Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/sdk/csp-rules ## Configuring Your Content Security Policy for Membrane's JavaScript SDK When using **Membrane's JavaScript SDK** in your application, you'll need to configure your **Content Security Policy (CSP)** to allow communication with our services. This article outlines the necessary CSP rules to add for proper functionality. *** ### Required CSP Rules Add the following domain to your Content Security Policy under these directives: * **connect-src** * **child-src** * **form-action** The domain to whitelist is: `api.getmembrane.com` *** ### Example CSP Configuration Here's an example of how your CSP rules might look after adding these directives: ```plaintext theme={null} Content-Security-Policy: connect-src 'self' api.getmembrane.com; child-src 'self' api.getmembrane.com; form-action 'self' api.getmembrane.com; // ... other existing rules ... ``` *** ### Why These Rules Are Needed * **connect-src**: Allows the SDK to make API calls to Membrane's servers. * **child-src**: Permits the loading of necessary resources from Membrane. * **form-action**: Required for submitting sensitive data to our API in certain cases. *** ### Additional Considerations There are no region-specific domains or additional rules required for the JavaScript SDK. However, it's always a good practice to verify these settings with your application security team to ensure they align with your organization's security policies. *** ### Further Resources For more information about our SDK, you can refer to the following documentation: * [SDK Documentation](https://console.getmembrane.com/ref/sdk/index.html) * [React Integration Documentation](https://console.getmembrane.com/ref/react/index.html) If you encounter any issues or have further questions about CSP configuration for Membrane, please don't hesitate to contact our support team. How to authenticate with Membrane and generate tokens. UI components and patterns for building connection experiences. Use React components to build your integration UI. # React SDK Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/sdk/react The React SDK provides hooks and pre-built UI components for building integration experiences in your React app. It's built on top of the [TypeScript SDK](/docs/ways-to-use-membrane/sdk). ## Install ```bash theme={null} npm install @membranehq/react ``` ## Setup Wrap your app with the Membrane provider: ```jsx theme={null} import { MembraneProvider } from '@membranehq/react' function App() { return ( { const response = await fetch('/api/membrane-token') const { token } = await response.json() return token }} > ) } ``` ## Hooks Use hooks to fetch and manage Membrane resources: ```jsx theme={null} import { useIntegrations, useConnections, useAction } from '@membranehq/react' function IntegrationsPage() { const { items: integrations } = useIntegrations() const { items: connections } = useConnections() return (
{integrations?.map((integration) => (
{integration.name}
))}
) } ``` ### Available Hooks | Hook | Description | | - | - | | `useMembrane` | Get the MembraneClient instance from context | | `useIntegrations` / `useIntegration` | List or get integrations | | `useConnections` / `useConnection` | List or get connections | | `useActions` / `useAction` | List or get actions | | `useActionInstances` / `useActionInstance` | List or get action instances (per-tenant) | | `useFlows` / `useFlow` | List or get flows | | `useFlowInstances` / `useFlowInstance` | List or get flow instances | | `useFlowRuns` / `useFlowRun` | List or get flow run history | | `useDataSources` / `useDataSource` | List or get data sources | | `useDataSourceInstances` / `useDataSourceInstance` | List or get data source instances | | `useFieldMappings` / `useFieldMapping` | List or get field mappings | | `useFieldMappingInstances` / `useFieldMappingInstance` | List or get field mapping instances | | `useDataLinkTables` / `useDataLinkTable` | List or get data link tables | | `useDataLinkTableInstances` / `useDataLinkTableInstance` | List or get data link table instances | | `useAppEventTypes` / `useAppEventType` | List or get internal event types | | `useAppEventSubscriptions` / `useAppEventSubscription` | List or get internal event subscriptions | | `useAppDataSchemas` / `useAppDataSchema` | List or get app data schemas | | `useAppDataSchemaInstances` / `useAppDataSchemaInstance` | List or get app data schema instances | | `useExternalEventSubscriptions` / `useExternalEventSubscription` | List or get external event subscriptions | | `useCustomers` / `useCustomer` | List or get customers | | `usePackages` / `usePackage` | List or get packages | | `useScreen` | Access UI screens | | `useConnectorSpec` | Get connector specification | | `useDataCollectionSpec` | Get data collection specification | ## UI Components Pre-built components for common integration UIs: ```jsx theme={null} import { Integrations, DataBuilder } from '@membranehq/react' // Render a full integrations list // Render a data builder for configuring field mappings ``` See the sub-pages for detailed reference on [Hooks](/docs/ways-to-use-membrane/sdk/react/hooks), [UI Components](/docs/ways-to-use-membrane/sdk/react/ui-components), and more. # Combobox Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/sdk/react/combobox The `ComboBox` component is used internally for the [`DataForm`](/docs/ways-to-use-membrane/sdk/react/data-input) UI.\ It handles each input with dropdown, tags, formula configuration, etc. It is possible to use it without `DataForm` by passing `options` or `optionFactories`. Additionally, `variablesSchema` is supported. Here is the example with static options and local state: ```javascript theme={null} import { ComboBox, ComboBoxOption } from "@membranehq/react"; const staticOptions: ComboBoxOption[] = [ { label: 'Option 1', value: 'option-1', }, { label: 'Option 2', value: 'option-2', }, ] export function ComboboxExample() { const [value, setValue] = useState(null) return ( ) } ```
## Styling component parts Styling is supported, see [Components](/docs/ways-to-use-membrane/sdk/react/ui-components) page for more details. The `ComboBox` component is made of multiple smaller components, here is a list of parts for each: **Component:**\ dropdown | Part className | Description | | - | - | | `dropdown__popover-container` | Main container of a dropdown. Emits keyboard navigation events | | `dropdown__content` | Dropdown content container | | `dropdown__panel` | Section panel. Can be multiple. Can be nested | | `dropdown__search-input` | Search input element | | `dropdown__search-divider` | Divider between search input and rest of elements | | `dropdown__list` | Items list container | | `dropdown__items-list-header` | Items list header | | `dropdown__item` | Single list item container | | `dropdown__item-title` | List item title | | `dropdown__no-options-heading` | Heading title for empty search result | | `dropdown__error-heading` | Heading for error when options failed to load | | `dropdown__error-heading-icon` | Error heading icon | | `dropdown__info-heading` | Heading for unsupported states within the dropdown | | `dropdown__info-heading-icon` | Info heading icon | | `dropdown__loading-item-container` | List item skeleton container | | `dropdown__loading-item-icon` | List item icon skeleton | | `dropdown__loading-item-title` | Loading item title skeleton | # Data Input Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/sdk/react/data-input Data Input is used to let users enter data that fits a specific [Data Schema](/docs/references/data-schemas) with optional use of variables defined by another Data Schema. It supports complex schemas, lookups, data transformation formulas, and other features useful for configuring integrations. ```javascript theme={null} import { DataInput, DataSchema } from "@membranehq/react"; export function PlainDataInputExample() { const [value, setValue] = useState({}) const schema = { type: 'object', properties: { String: { type: 'string', format: 'hostname', }, Integer: { type: 'integer', }, Boolean: { type: 'boolean', }, Object: { type: 'object', required: ['String'], properties: { NoType: { title: 'No Type Title', }, String: { type: 'string', }, Object: { type: 'object', properties: { String: { type: 'string', }, }, }, }, }, }, required: ['String', 'Boolean'], } const variablesSchema: DataSchema = { type: 'object', properties: { 'string-value': { type: 'string', title: 'String Value', }, 'array-value': { type: 'array', title: 'Array Value', items: { type: 'string', }, }, numberOfEmployees: { type: 'number', }, currentWeekNumber: { type: 'integer', }, createdTime: { type: 'string', format: 'date-time', }, }, } return ( setValue(importValue)} /> ) } ```
## Nested Components * [Combobox](/docs/ways-to-use-membrane/sdk/react/combobox) # Hooks Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/sdk/react/hooks Hooks let you interact with Membrane resources from React components. They handle async operations and state management automatically. Get the Membrane client instance with `useMembrane()`: ```jsx theme={null} import { useMembrane } from '@membranehq/react' function MyComponent() { const membrane = useMembrane() // Use membrane.ui.connect(), membrane.action(), etc. } ``` ## Hooks for individual entities Hooks for individual entities are used to fetch and manage data for a single workspace element such as [Flow](/reference/workspace-elements/flows) or [Field Mapping](/reference/workspace-elements/field-mappings). They have the following structure: ```jsx theme={null} useFlow(selector: FlowSelector): { flow: undefined | Flow; apply: ((integrationKeys: string[]) => Promise); reset: (() => Promise); refresh: (() => Promise); accessor: undefined | FlowAccessor; loading: boolean; saving: boolean; error: any; refreshing: boolean; create: ((data: CreateFlowRequest) => Promise); patch: ((data: Partial) => Promise); put: ((data: UpdateFlowRequest) => Promise); archive: (() => Promise); } ``` Here is what each part of it means:
selector String or object that uniquely identifies the entity. It can be the entity's ID or a combination of entity key and connection selector.
flow Current state of the entity
apply Applies universal entity to selected integrations
reset Resets entity to its initial state (only works for entities that have parents)
refresh Refresh entity data from the server
accessor SDK-provided object for working with the entity.
loading Whether initial data is being loaded
saving Whether entity is being saved to the server
error Error that occurred during last operation
refreshing Whether entity is being refreshed from server
create Create new entity
patch Partially update entity
put Fully update entity (replace all its properties with provided ones)
archive Archive (soft delete) entity
## Hooks for lists Hooks for fetching lists of entities look like this: ```jsx theme={null} useFlows(query?): { items: Flow[]; refresh: (() => Promise); refreshing: boolean; loadMore: (() => Promise); loadingMore: boolean; loading: boolean; error: any; } ``` Here is what each part of it means:
query Query for fetching / filtering the list of entities
items Current list of entities (including all the loaded pages)
loadMore Load the next page of entities from the server (if there is no more pages - nothing will happen)
loadingMore Whether more entities are being loaded from the server
refresh Refresh the list of entities from the server (if multiple pages were fetched previously - only the first page will remain after the refresh)
loading Whether the list is being loaded from the server
error Error that occurred during the last operation
## TypeScript Tips Hooks return fully typed data. Use the exported types for additional type safety: ```typescript theme={null} import { useConnections, useMembrane } from '@membranehq/react' import type { Connection } from '@membranehq/sdk' function ConnectionList() { const { items, loading, error } = useConnections() // items: Connection[] | undefined // loading: boolean // error: Error | undefined if (loading) return
Loading...
if (error) return
Error: {error.message}
return (
    {items?.map((conn: Connection) => (
  • {conn.name} {conn.disconnected ? '(disconnected)' : ''}
  • ))}
) } ``` ## List of hooks You can find the full list of hooks and their signatures in the [React Library Reference](https://console.getmembrane.com/ref/react/index.html) # Portal Boundary Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/sdk/react/portal-boundary Most of floating components in `@membranehq/react`,\ like [Combobox](/docs/ways-to-use-membrane/sdk/react/combobox), use `Portal` to mount outside of the stacking context. If you are using a floating component on the default stacking context, no additional setup is required\ as the default portal boundary is the `` element. But for cases when you want to use a floating component within another floating component, the stacking context needs to be adjusted by bounding the portal area.\ This can be done with the `FloatingPortalBoundary` component. A common situation is a custom `Modal` rendered in a `Portal` with a [Combobox](/docs/ways-to-use-membrane/sdk/react/combobox) as part of [DataInput](/docs/ways-to-use-membrane/sdk/react/data-input). ```javascript theme={null} import { DataInput } from "@membranehq/react"; import { Modal } from './my-modal' const schema = {...} const variablesSchema = {...} export function ModalWithDataInput() { return ( Create record ) } ``` With this setup both `Modal` and `Combobox` are rendered over their respective `Portal` inside of the ``,\ resulting in two separate stacking contexts and `Modal` might appear above `Combobox`. Portal Boundary illustration To fix this problem we should bound the `Combobox` portal with `Modal.Content`.\ Use `FloatingPortalBoundary` to wrap the content container. > Note: `FloatingPortalBoundary` should only have a single child because it attaches a `data-*` attribute to it,\ > which serves as an anchor for `Portal`. ```javascript theme={null} import { DataInput, FloatingPortalBoundary } from "@membranehq/react"; import { Modal } from './my-modal' const schema = {...} const variablesSchema = {...} export function ModalWithDataInput() { return ( Create record ) } ``` Now, as the content has its own boundary, the `Combobox` from `DataInput` is portalled into the correct stacking context. Portal Boundary illustration
## Props: `FloatingPortalBoundary`
Prop Type Description
children React.ReactElement Components Children. Accepts only single child.
id string | undefined Custom ID for boundary container. If not passed - useId is used.
# UI Components Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/sdk/react/ui-components > For now, you are expected to have [Tailwind Preflight](https://tailwindcss.com/docs/preflight) installed, this won't be necessary in future.\ > It **must be** imported before `@membranehq/react/styles.css`. Each component and its parts can be addressed by their unique class name. You can use them to style your components and interact with them. You can find a full list of parts and class names of a given component on its respective documentation page. ## Default Styles To apply default styles to components, simply import styles.css: ```tsx theme={null} import '@membranehq/react/styles.css' ``` ## Custom Styles All style layers exported by `styles.css` are prefixed with `iap-` value to prevent layers collision. ```css theme={null} @layer iap_reset, iap_base, iap_tokens, iap_recipes, iap_utilities; ``` Let's looks at the imaginary `button` component example: UI Components illustration ### Styling with CSS When using plain CSS it is enough to just use `className` of a part.\ Let's change button background color to red and both icons to black: ```css theme={null} /* styles.css */ @layer iap_reset, iap_base, iap_tokens, iap_recipes, iap_utilities; @layer iap_reset { @tailwind base; } .button__container { background: #e54d2e; } .button__container:hover { background: #dd4425; } .button__icon-left, .button__icon-right { background: #000; } ``` ```tsx theme={null} import { ExampleButton } from '@membranehq/react' import './styles.css' import '@membranehq/react/styles.css' const App = () => { return } ``` UI Components illustration **Example** Project with live CSS styling example ### Styling with Tailwind For projects that use Tailwind an extra step is required.\ To avoid layers order collision you need to define layers in your main `css` file. You can target part `className` by using [`@apply` directive](https://tailwindcss.com/docs/functions-and-directives#apply). In this example let's make button sharp and blue, then change the title text style to bold: ```css theme={null} /* styles.css */ @layer iap_reset, iap_base, iap_tokens, iap_recipes, iap_utilities; @layer iap_reset { @tailwind base; } @tailwind components; @tailwind utilities; .button__container { @apply rounded-none; } .button__title { @apply font-bold bg-blue-700; } ``` ```tsx theme={null} import { ExampleButton } from '@membranehq/react' import './styles.css' import '@membranehq/react/styles.css' const App = () => { return } ``` UI Components illustration **Example** Vite project with live Tailwind styling example Next.js project with Tailwind styling example # Vue.js Source: https://docs.getmembrane.com/docs/ways-to-use-membrane/sdk/vue # Add Javascript SDK to your Front-end First, install `@membranehq/sdk` NPM package: ```shell theme={null} npm install @membranehq/sdk ``` Then, initialize the SDK with an Authentication Token: ```javascript theme={null} import { MembraneClient } from '@membranehq/sdk' const membrane = new MembraneClient({ // Test authentication token. You will need to replace it with the real one. token: '', }) ``` In this example we use the test token you can find on the [Settings](https://console.getmembrane.com/w/0/settings) page of your workspace.\ You will need to replace it with a real authentication token later (see [Authentication](/docs/how-membrane-works/authentication)). To verify that everything works, let's open our drop-in integration UI: ```javascript theme={null} await membrane.open() ``` In the end your basic Vue.js setup may look like this: ```vue theme={null} ``` ## Dynamic Token If it is more convenient for you to fetch token dynamically instead of providing static value, you can use `fetchToken` instead: ```javascript theme={null} import { MembraneClient } from '@membranehq/sdk' const membrane = new MembraneClient({ // Test authentication token. You will need to replace it with the real one. async fetchToken() { return '' }, }) ``` This option also automatically handles token expiration. If SDK was initialized long ago and token had time to expire, it will be automatically re-fetched before making new API requests. ## SDK Reference To see the full list of SDK methods check out the [JavaScript API Reference](https://console.getmembrane.com/ref/sdk/index.html). # Workspace Migration Source: https://docs.getmembrane.com/reference/management-api/workspace-migration # Workspace Provisioning Source: https://docs.getmembrane.com/reference/management-api/workspace-provisioning Programmatically create and configure workspaces for your organization. This is useful for multi-tenant SaaS platforms that need to provision isolated integration environments for their own customers. These endpoints require organization-level authentication (platform user tokens), not workspace-level tokens. ## Key Concepts ### Organizations An **Organization** (Org) is the top-level entity that groups your team and workspaces. Org members share billing, authentication, and access controls. Organizations exist on the **Hub** — the central management layer. ### Org Workspaces An **Org Workspace** represents a workspace as seen from the organization level. It stores the workspace's name, key, secret, and org membership. Org workspaces also exist on the **Hub** and are used for provisioning, access control, and credential management. ### Workspaces A **Workspace** is the runtime environment where integrations, connections, and tenant data live. Workspaces store the actual configuration — settings, webhook URIs, public keys, and tenant data. In a multi-instance deployment, each workspace runs on a specific **Instance**. ## Architecture ``` ┌─────────────────────────────────────────────────┐ │ Hub │ │ │ │ ┌───────────┐ │ │ │ Org │ │ │ └─────┬─────┘ │ │ │ has many │ │ ┌─────┴──────────────┐ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ OrgWorkspace │ │ OrgWorkspace │ │ │ │ (Production) │ │ (Development)│ │ │ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ └────────┼──────────────────┼───────────────────────┘ │ maps to │ maps to ┌────────┼──────────┐ ┌───┼────────────────┐ │ Instance A │ │ Instance B │ │ ┌────▼────────┐ │ │ ┌────▼────────┐ │ │ │ Workspace │ │ │ │ Workspace │ │ │ │ (settings, │ │ │ │ (settings, │ │ │ │ tenants, │ │ │ │ tenants, │ │ │ │ data) │ │ │ │ data) │ │ │ └─────────────┘ │ │ └─────────────┘ │ └───────────────────┘ └────────────────────┘ ``` In a **single-instance** (cloud) setup, the hub and instance are the same server, so org workspaces and workspaces are always co-located. The separation matters in **multi-instance** (self-hosted) deployments, where: * The **Hub** manages organizations, org workspaces, user access, and credential rotation centrally. * Each **Instance** runs one or more workspaces with their integration data, connections, and tenant state. * The Management API operates on the Hub and routes workspace-level configuration changes to the appropriate instance. ## Typical Workflow 1. **Create** a workspace via `POST /org-workspaces` with a name and org ID. 2. **Configure** it via `PATCH /org-workspaces/{id}` — set webhook URIs, public keys, settings, etc. 3. **Generate an access token** via `POST /org-workspaces/{id}/access-token` to authenticate API calls scoped to that workspace. 4. **Rotate secrets** via `POST /org-workspaces/{id}/rotate-secret` when needed. 5. **Archive/restore** workspaces as your customers come and go. # Authentication Source: https://docs.getmembrane.com/reference/overview/authentication This reference contains a list of all the endpoints available in the integration.app API, their inputs and outputs. ## Authentication When making requests to the API, you need to provide a **Bearer** token in the **Authorization** header. You can find a test token in your [workspace settings](https://console.integration.app/w/0/settings/testing). To learn how to generate production tokens, check the [Authentication](/docs/how-membrane-works/authentication) documentation article. Some endpoints, such as editing users require **admin-level** access. You can find details on how to generate an admin token [here](/docs/how-membrane-works/authentication#admin-token). # Element Selectors Source: https://docs.getmembrane.com/reference/overview/element-selectors To simplify interacting with membrane workspace elements, the API supports multiple ways of addressing them via **selectors** (`:selector` in the API route) and query parameters. ## By ID If you have an id of an element you need, you can use it to address the element: * `GET /actions/651488f5eba1bdf86a702ca3` - get action with id 651488f5eba1bdf86a702ca3. * `POST /flows/63b30e09e9a70700c7dc5e82/run` - run flow with id 63b30e09e9a70700c7dc5e82. When you know an element id, you should always use this way of addressing it - its the most unambiguous one. When addressing elements by id, you should not pass any additional parameters (integrationId, connectionId, etc). ## By Key Most workspace elements have keys act as human-readable identifiers of those elements. In many cases, you can address elements by key: * `GET /actions/create-task` - get action with key "create-task". * `POST /flows/send-update/run` - run a flow with key "send-update". Addressing elements by key will work when: * There is only one element with this key in the workspace. * There is a universal element with this key and it can be used for the current request. Addressing elements by key will not work when there are multiple elements with the same key and it's not possible to guess which one you need, for example: * `GET /actions/create-task` will not work when you have multiple integration-level actions with key `create-task` and no universal one. * `POST /flows/send-update/run` will not work without additional clarifying parameters if the current tenant has multiple connections (which makes it impossible to determine which of them to use to run the flow). ## By Integration Key + Element Key It is the best way to address integration-specific elements. For example: * `GET /actions/github.create-task` will return action with key `create-task` for integration with key `github`. * `POST /flows/slack.send-update/run` will run flow with key `send-update` for integration `slack` (using default connection to Slack for the current user). Addressing by integration key + element key lets you address integration-specific elements even when universal element with the same key exist. For example, if you have universal action `create-task` and Github connector provides github-specific action `create-task`, by addressing action as `github.create-task` you are guaranteed to get the github-specific version of the action (and not an implementation of universal `create-task` for github). ## Parameters Additionally to selector in the path, there are clarifying parameters you can pass to get the exact element you need when addressing it by key: * `connectionId` - will return connection-specific element for this id (and automatically create it from integration-specific element if it doesn't exist yet). * `integrationKey` or `integrationId` - will return integration-specific implementation of a given universal element if it exist (it will not be auto-created). For example, `/flows/send-update?integrationKey=slack` will address an implementation of universal flow `send-update` for `slack` integration. * `layer` (universal|integration|connection) - will return element of the specific layer (see [Overview](/docs/getting-started/overview) for details). For example, `/actions/github.create-task?layer=connection` will return connection-layer task based on the integration-layer task. The connection-layer task will be returned for the default `github` connection for the current tenant (if it can be determined). # API Errors Source: https://docs.getmembrane.com/reference/overview/errors # API Error Reference The Membrane API uses a standardized error format to provide clear, actionable information when requests fail. This guide covers the error structure, types, and common error scenarios. ## Error Response Format All API errors follow a consistent JSON structure: ```json theme={null} { "type": "error_type", "key": "error_key", "message": "Human-readable error description", "data": { // Additional context-specific data }, "causedByError": { // Nested error object if this error was caused by another undelrying error } } ``` ### Response Fields | Field | Type | Description | | - | - | - | | `type` | string | The category of error (see Error Types below) | | `key` | string | Specific error identifier within the type | | `message` | string | Human-readable error description | | `data` | object | Additional context-specific information | | `causedByError` | object | Nested error that caused this error | ## HTTP Status Codes The API maps error types to appropriate HTTP status codes: | Status Code | Error Key | Description | | - | - | - | | 400 | (default) | Bad Request - Invalid input or request | | 401 | `not_authenticated` | Unauthorized - Authentication required | | 403 | `access_denied` | Forbidden - Insufficient permissions | | 404 | `not_found` | Not Found - Resource doesn't exist | | 429 | `rate_limit_exceeded` | Too Many Requests - Rate limit exceeded | | 500 | `internal` | Internal Server Error | ## Error Types ### BAD\_REQUEST **Type:** `bad_request` Something is wrong with the input received by the API. This includes validation errors, missing required fields, or invalid data formats. **Common Keys:** * `not_found` - The requested resource doesn't exist * `not_authenticated` - Authentication credentials are missing or invalid * `access_denied` - User lacks permission for this operation * `already_exists` - Resource with the same identifier already exists * `rate_limit_exceeded` - Too many requests in a given time period **Example:** ```json theme={null} { "type": "bad_request", "key": "not_found", "message": "Connection with id 'conn_123' not found" } ``` ### CONNECTION **Type:** `connection` An error related to a Connection's authentication or communication with an external system. **Common Keys:** * `connection_is_defunct` - Connection is no longer valid * `access_token_expired` - OAuth token needs refresh * `rate_limit_exceeded` - External API rate limit hit **Example:** ```json theme={null} { "type": "connection", "key": "access_token_expired", "message": "Access token for Salesforce connection has expired", "data": { "connectionId": "conn_sf_123", "connectorKey": "salesforce" } } ``` ### CONFIGURATION **Type:** `configuration` Incorrect or broken configuration in flows, actions, or other configurable elements. **Example:** ```json theme={null} { "type": "configuration", "message": "Invalid field mapping: source field 'customer.email' does not exist", "data": { "fieldMappingId": "fm_123", "missingField": "customer.email" } } ``` ### CUSTOM\_CODE **Type:** `custom_code` Error in custom JavaScript code execution. **Example:** ```json theme={null} { "type": "custom_code", "message": "JavaScript execution error: ReferenceError: variable is not defined", "data": { "lineNumber": 15, "columnNumber": 8, "code": "const result = undefinedVariable + 1" } } ``` ### CONCURRENCY **Type:** `concurrency` Errors related to concurrent operations and resource locking. **Common Keys:** * `lock_timeout` - Failed to acquire lock within timeout period **Example:** ```json theme={null} { "type": "concurrency", "key": "lock_timeout", "message": "Could not acquire lock for connection within 30 seconds", "data": { "resourceId": "conn_123", "timeoutMs": 30000 } } ``` ### INTERNAL **Type:** `internal` Unexpected internal errors. These are automatically reported to monitoring systems. **Example:** ```json theme={null} { "type": "internal", "message": "An unexpected error occurred. Please try again or contact support.", "data": { "errorId": "err_xyz789" } } ``` # Act Source: https://docs.getmembrane.com/reference/workspace-elements/act POST /act `POST /act` is Membrane's unified entry point for running an action through a connection. See the [Actions guide](/docs/how-membrane-works/actions) for the conceptual walkthrough — this page is the endpoint reference. ## Dispatch styles Exactly **one** of `id`, `key`, `api`, or `code` per call. Sending more than one returns `400`. | Field | Style | When to use | | - | - | - | | `api` | Inline `api` | Make an ad-hoc HTTP request through the connection's auth and base URL. | | `code` | Inline `code` | Run a JavaScript snippet in a sandbox with a pre-wired authenticated `membrane` client. | | `key` | Reusable | Run a saved (workspace- or integration-level) action by its stable key. | | `id` | Reusable | Run a catalog action or a saved action by its Membrane ID. | ## Resolving a connection Every dispatch needs a connection. Membrane resolves one in this order: 1. `connectionId` — direct. 2. `connectionKey` — look up by key. 3. `integrationId` or `integrationKey` — use that integration's default connection. 4. Workspace default connection. Reusable-action dispatches that target a connection-scoped or integration-scoped action use the action's own scope automatically — the body fields above only kick in when the action's scope doesn't already resolve a connection. For inline dispatches (`api` / `code`), the connection must resolve to something — otherwise the request returns `400`. ## Request body ```json api theme={null} { "connectionKey": "hubspot-prod", "api": { "method": "POST", "path": "/crm/v3/objects/contacts", "body": { "properties": { "email": "jane@example.com" } } } } ``` ```json code theme={null} { "connectionKey": "hubspot-prod", "code": "module.exports = async ({ input, membrane }) => ({ ok: true })", "input": {} } ``` ```json key theme={null} { "key": "create-contact", "integrationKey": "hubspot", "connectionKey": "hubspot-prod", "input": { "email": "jane@example.com" } } ``` ```json id theme={null} { "id": "action_abc", "connectionKey": "hubspot-prod", "input": { "email": "jane@example.com" }, "meta": { "source": "support-ticket-1234" } } ``` | Field | Type | Description | | - | - | - | | `api` | object | Inline HTTP spec — `{ method, path, body?, headers?, query? }`. | | `code` | string | Inline JS module body, executed in a sandbox. | | `key` | string | Action key. Scope with `integrationKey`/`integrationId`/`connectionId` if non-unique. | | `id` | string | Action ID. Mutually exclusive with `key` / `api` / `code`. | | `connectionId` | string | Connection to run against. See [resolution priority](#resolving-a-connection). | | `connectionKey` | string | Connection key alternative to `connectionId`. | | `integrationId` | string | Integration ID — used to scope `key` lookups or pick a default connection. | | `integrationKey` | string | Integration key alternative to `integrationId`. | | `input` | object | Action input. For reusable-action dispatches, validated against the action's `inputSchema`. | | `meta` | object | Arbitrary metadata stored on the resulting [Action Run Log](/reference/workspace-elements/action-run-logs). | ## Response ```json theme={null} { "output": { "id": "12345", "email": "jane@example.com" }, "actionRunId": "" } ``` Every response — success or failure — carries an `actionRunId`. Pass it to [`GET /action-run-logs/{id}`](/reference/workspace-elements/action-run-logs) to inspect the resolved action, mapped input, raw HTTP exchange, and any errors. On failure the body still includes `actionRunId` alongside the error. ## Replay from a run log Every action run log carries an `action` field — the complete `/act` body that was used. Pass it back unchanged to re-run the same dispatch: ```bash theme={null} SNAPSHOT=$(curl -s https://api.integration.app/action-run-logs/ \ -H "Authorization: Bearer $TOKEN" | jq '.action') curl -X POST https://api.integration.app/act \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "$SNAPSHOT" ``` The snapshot is the source of truth for "what ran". Top-level `input` and `connectionId` on the run log are deprecated — read them from `action` instead. ## Legacy: `POST /actions/{selector}/run` `POST /actions/{selector}/run` is the older endpoint for running a saved action. It still works for existing callers but is **deprecated** — `/act` covers all the same cases plus inline `api` and `code` dispatch. | Before | After | | - | - | | `POST /actions//run` body: `{ ...input }` | `POST /act` body: `{ "id": "", "input": {...}, "connectionKey": "..." }` | | `POST /actions//run?connectionId=` | `POST /act` body: `{ "key": "", "connectionId": "", "input": {...} }` | | Ad-hoc HTTP via a custom `run-javascript` action | `POST /act` body: `{ "api": { "method": "...", "path": "..." }, "connectionKey": "..." }` | Use `/act` for new code. # Action Run Logs Source: https://docs.getmembrane.com/reference/workspace-elements/action-run-logs Every call to [`POST /act`](/reference/workspace-elements/act) (or its [legacy predecessor `POST /actions/{selector}/run`](/reference/workspace-elements/act#legacy-post-actionsselectorrun)) produces an Action Run Log — the durable record of what ran, what came back, and how long it took. ## Action snapshot Each log carries an `action` field — the complete `/act`-body that was used. POST it straight back to `/act` to re-run the same dispatch: ```bash theme={null} SNAPSHOT=$(curl -s https://api.integration.app/action-run-logs/ \ -H "Authorization: Bearer $TOKEN" | jq '.action') curl -X POST https://api.integration.app/act \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "$SNAPSHOT" ``` The snapshot is the source of truth for "what ran". The top-level `input` and `connectionId` fields on each log are **deprecated** — read them from `action.input` / `action.connectionId` instead. See the [Actions guide](/docs/how-membrane-works/actions) for the full pick → run → replay loop. # Actions Source: https://docs.getmembrane.com/reference/workspace-elements/actions Action represents a single request or query your application wants to make in an external application. For example, "Create a task", "Get list of users", or "Send a message". Here is an example of an action: ```yaml theme={null} name: Create Task key: create-task inputSchema: type: object properties: title: type: string description: type: string type: create-data-record config: dataSource: collectionKey: issues fieldMapping: defaultValue: summary: $var: $.input.title description: $var: $.input.description ```
## Action Structure Action has the following properties: * **`inputSchema`** – [Data Schema](https://.integration.app/docs/references/data-schemas) describing the payload that will be validated when you run the action. * **`type`** – action type that maps to a [Function Type](/docs/references/functions) that implements the `run` function of this action. * **`config`** – implementation of the `run` function. * **`outputSchema`** - defines expected output of the action. * **`customOutputSchema`** - overrides the default value of `outputSchema`. If customOutputSchema is set, outputSchema will always equal `customOutputSchema` ## Run Function Action interface provides one function: [Run](/docs/references/functions/function-types/run-action). The implementation of this function is configured with two properties: `type` and `config`. The `run` function receives the following arguments: * `input` - action input provided when running the action. Action types map to [Function Types](/docs/references/functions) and let you choose how action's `run` function is implemented. If not sure what action type to use, [Javascript](/docs/references/functions/function-types/javascript) is the most versatile type that lets you do anything, and [API Request to External App](/docs/references/functions/function-types/api-request-to-external-app) is commonly used to configure API requests if it's all that action needs to do. | Type | Description | Function Type | | :- | :- | :- | | `run-javascript` | Runs custom JavaScript (or TypeScript) code | [JavaScript](/docs/references/functions/function-types/javascript) | | `api-request-to-external-app` | Makes API requests to the external app with authentication applied automatically | [API Request to External App](/docs/references/functions/function-types/api-request-to-external-app) | | `api-request-to-your-app` | Makes API requests to your Internal API | [API Request to Your App](/docs/references/functions/function-types/api-request-to-your-app) | | `http-request` | Makes arbitrary HTTP requests to any endpoint | [HTTP Request](/docs/references/functions/function-types/http-request) | | `list-data-records` | List records in a data collection page by page | [List Data Records](/docs/references/functions/function-types/list-data-records) | | `find-data-record-by-id` | Find a record in a data collection by ID | [Find Data Record By ID](/docs/references/functions/function-types/find-data-record-by-id) | | `search-data-record` | Search data records by a query string | [Search Data Record](/docs/references/functions/function-types/search-data-record) | | `match-data-record` | Find a data record with fields matching a query | [Match Data Record](/docs/references/functions/function-types/match-data-record) | | `create-data-record` | Create a new data record | [Create Data Record](/docs/references/functions/function-types/create-data-record) | | `update-data-record` | Update an existing data record | [Update Data Record](/docs/references/functions/function-types/update-data-record) | | `delete-data-record` | Delete a data record by id | [Delete Data Record](/docs/references/functions/function-types/delete-data-record) |
## Using Actions Action has only one function: `run`. To run an action, you need to provide an input that matches its `inputSchema`. As a result, you will get an output that matches its `outputSchema`. All the action runs are recorded as [Action Run Logs](/reference/workspace-elements/action-run-logs). See full API reference here: [Actions API Reference](https://.integration.app/update/reference/workspace-elements/actions). ## API Operations | Operation | Method | Endpoint | Description | | - | - | - | - | | [Act](/reference/workspace-elements/act) | POST | `/act` | Run an action (canonical run path) | | [List all actions](/api-reference/actions/list-actions) | GET | `/actions` | Retrieve all workspace actions | | [Get action by ID](/api-reference/actions/get-action-by-selector) | GET | `/actions/{selector}` | Get specific action details | | [Create a new action](/api-reference/actions/create-a-new-action) | POST | `/actions` | Create a new workspace action | | [Replace action](/api-reference/actions/replace-action-completely) | PUT | `/actions/{selector}` | Replace action configuration | | [Patch action](/api-reference/actions/update-action-partially) | PATCH | `/actions/{selector}` | Update action partially | | [Clone action](/api-reference/actions/clone-action) | POST | `/actions/{selector}/clone` | Clone an existing action | | [Run action](/reference/workspace-elements/act#legacy-post-actionsselectorrun) | POST | `/actions/{selector}/run` | Execute an action (**deprecated** — use `/act`) | | [Reset action](/api-reference/actions/reset-action) | POST | `/actions/{selector}/reset` | Reset action to default state | | [Export action](/api-reference/actions/export-action-flat-interface) | GET | `/actions/{selector}/export` | Export action configuration | | [Get action dependencies](/api-reference/actions/get-dependencies-of-action-flat-interface) | GET | `/actions/{selector}/dependencies` | Get action dependencies | | [Restore action](/api-reference/actions/restore-action-flat-interface) | POST | `/actions/{selector}/restore` | Restore archived action | | [Refresh actions](/api-reference/actions/refresh-action) | POST | `/actions/refresh` | Refresh action definitions | | [Archive action](/api-reference/actions/archive-action) | DELETE | `/actions/{selector}` | Archive an action | # App Data Schemas Source: https://docs.getmembrane.com/reference/workspace-elements/app-data-schemas App Data Schemas is a [Data Schema](/docs/references/data-schemas) that can be shared across multiple places in your integrations.\ It can be used for [Field Mappings](/reference/workspace-elements/field-mappings) or as a component for any other schema used in an integration. ## Referencing Schema You can reference an App Data Schema in other schemas using the `$ref` formula: ```json theme={null} { "type": "object", "properties": { "name": { "type": "string" }, "user": { "$ref": "schemas/user" // Reference to the "user" App Data Schema } } } ``` ## User-specific Schema Whenever a schema is used in a context of a specific user, and you provided an HTTP request, it will be executed on behalf of the current user. The response to this request should be a JSON-encoded [Data Schema](/docs/references/data-schemas). You can find the latest request and response for a given user on the [App Data Schema Instances](https://console.integration.app/w/0/deployments/app-data-schema-instances) page. # Internal Event Logs Source: https://docs.getmembrane.com/reference/workspace-elements/app-event-logs # Internal Events Source: https://docs.getmembrane.com/reference/workspace-elements/app-events Internal Events let Membrane know when something happens in your app. They can be used to trigger Flows using [Internal Event Trigger](https://membrane-docs.readme.io/docs/nodes#/app-event-trigger). ## Internal Event Types To use Internal Events, you need to define one or more Internal Event Types. You can do it on the [Internal Event Types](https://console.integration.app/w/0/event-types) page. Each Internal Event Type has: * **Schema** – a [Data Schema](/docs/references/data-schemas) describing the event's contents. * **Example** – an example of the event's contents matching the Schema. You can use Example to generate schema. ## Sending Events to Membrane ### Global Webhook Use Global Webhook to send internal events to Membrane in a centralized (not per-user) way. Simply make an HTTP request of any type to the webhook URI and the event will be captured and processed. ```bash theme={null} curl --request POST \ --url https://api.integration.app/{GLOBAL_WEBHOOK_URI} \ --header "Authorization: Bearer {CUSTOMER_TOKEN}" \ --header "Content-Type: application/json" \ --data '{"eventType": "order.created", "orderId": "123"}' ``` **Associating Events with Tenants** When you send an event to the Global Webhook, Membrane needs to know which tenant the event belongs to. There are three ways to specify this, in order of priority: 1. **Authentication Token (Recommended)** – Include the tenant's authentication token in the `Authorization` header. Membrane will automatically extract the tenant ID from the token. 2. **Tenant ID Formula** – Configure a formula in the Internal Event Type settings that extracts the tenant ID from the event payload. This is useful when you cannot include an authentication token in the request. 3. **User ID Formula (Legacy)** – For backward compatibility, you can use the older "User ID Formula" field which works the same way as Tenant ID Formula. If none of these methods provide a tenant ID, the event will be rejected. ### Per-Tenant Subscriptions If instead of a Global Webhook, you want to use per-user webhooks or if you already have an API that allows per-user event subscriptions, you can configure subscriptions for your Internal Event. When configured, Membrane: 1. Makes an HTTP request to your application's subscription API 2. Provides a unique webhook URL in the request (available as `{{webhookUri}}` variable) 3. Expects your application to send events to that specific webhook URL 4. Tracks the subscription request and response for troubleshooting You can use [Tenant Fields](/docs/how-membrane-works/tenants) and `webhookUri` variables to configure the request. The request will be called the first time this Internal Event Type is used for a given User. You can find the subscription request and response, as well as other information about subscriptions on the [Subscriptions](https://console.integration.app/w/0/deployments/app-event-subscriptions) page. ### Batch Events If the body of the request your app sends to Membrane is an array, it will be treated as multiple events. For global webhooks with batch events, each event can be associated with a different tenant by including tenant identification in each event payload. ## How Subscriptions Work Internal event subscriptions are created when a [Flow](/reference/workspace-elements/flows) has a trigger that reacts to Internal Events and the flow is enabled. When you enable a Flow that uses an Internal Event Trigger node, Membrane automatically: 1. Creates an Internal Event Subscription for the specified Internal Event and Tenant. 2. If configured, makes an HTTP subscription request to your application's API. 3. Generates a unique webhook URL for receiving events. The subscription is automatically disabled (and unsubscribed) when there are no more flows that need it. ### Automatic De-duplication Membrane automatically de-duplicates subscriptions. If multiple Flow Instances need the same Internal Event Type for the same tenant and instance key, only one subscription is created and shared between them. For example: * Flow A and Flow B both trigger on "Order Created" events for Tenant X * Only one Internal Event Subscription is created * Both flows are launched when an event arrives * The subscription remains active until both flows are disabled ## Monitoring and Troubleshooting ### Viewing Subscriptions Navigate to [Internal Event Subscriptions](https://console.integration.app/w/0/deployments/app-event-subscriptions) to see: * All active and inactive subscriptions * Subscription status (subscribed/unsubscribed) * The webhook URL for each subscription * Subscription and unsubscription request/response history * Associated Internal Event Type and tenant ### Viewing Events Navigate to [Internal Events](https://console.integration.app/w/0/activity/app-events) to see: * All received events * Which subscription received each event * Event payload and timestamp * Which Flows were triggered * Execution status of triggered Flows ### Flow Was Not Launched If you expected a webhook to launch a flow, but it didn't: **Step 1: Check Logs** Go to the [Webhook Logs](https://console.integration.app/w/0/activity/external-webhook-logs) and check if the events you expected are there. Use filters to narrow down the list. If you found the record, check its details: * Is it associated to the correct App User? * Does it have the data you expected? **Step 2: Check Flow Node Configuration** If you found the event in logs but it didn't trigger the expected flow: * Does the trigger use the right webhook? Check node dependencies on the Flow Instance page. * Does it have a filter that excludes the event? ### Webhook Events Do Not Appear in Logs **Step 1: Check Subscription** If you are using a webhook with subscription, find the subscription by Webhook and User, and check its status. Is it subscribed successfully? **Step 2: Trigger the Webhook Manually** Make an HTTP request to the webhook URL: * If using a subscription – find the webhook URL on the corresponding subscription page. * If not – use the URL from the webhook page. ### Webhook Subscription Is Not Created Find it on the [Subscriptions](https://console.integration.app/w/0/deployments/app-event-subscriptions) page and check the latest request and response. If everything looks correct, the problem is likely on your app's side. You can try deleting the subscription and creating it again to troubleshoot. # Connections Source: https://docs.getmembrane.com/reference/workspace-elements/connections Connections represent credentials and configuration used to access integrations on behalf of your tenants. They have the following properties: | Property | Description | | - | - | | `id` | Unique identifier of the connection. | | `name` | Name of this connection. It can be set by you when creating the connection or assigned automatically. | | `type` | Connection backing. Omitted (or `connector`) for integration-backed connections; `mcp` for a connection to an arbitrary [MCP server](#mcp-connections). | | `integrationId` | Identifier of the integration connection is created for. Not set for `mcp` connections. | | `authOptionKey` | If integration offers multiple authentication options, this property will contain the key of the selected option. | | `input` (secret) | Connection input provided by the tenant when creating the connection. | | `credentials` (secret) | Credentials required to access the external API. | | `connectorParametres` (secret) | Connector parameters provided by the integration at the time of connection creation. | | `disconnected` | Set to true if connection was disconnected (cannot be used to make API requests). | | `error` | Contains error details if connection is disconnected. | | `nextCredentialsRefreshAt` | Timestamp when credentials are scheduled to be automatically refreshed (if auto-refresh is enabled). | | `nextRetryTimestamp` | Timestamp when next reconnection attempt is scheduled (if connection is disconnected). | | `retryAttempts` | Current retry attempt count (0-10) for auto-reconnect. | ## Creating Connections > We recommend using [Connection UI](/docs/ways-to-use-membrane/embedded-ui/connection-ui/custom-connection-ui) implemented with our drop-in components or Javascript SDK.\ > Use the flow below only when you know what you are doing. To create a new connection or refresh credentials for an existing connection, follow the steps described below.\ They work in the same way for every connector regardless of the app that is being connected. ### Connector Auth Spec You can get information about how authentication works in a given connector from its specification. Look for the following fields: * `auth.type` - type of authentication supported by the connector. Possible values are: * `client-credentials` - Authentication based on credentials user enters in the UI. * `oauth1` - OAuth1 authentication * `oauth2` - OAuth2 authentication * `ui` - information about what to display user in the UI when authenticating. ### Authentication Flow To authenticate a user you need to do the following: 1. Collect connection parameters from the user. They need to match `ui.schema`. 2. Send a **POST** request to `https://api.getmembrane.com/connect` with the parameters described below. If `auth.type` is `client-credentials`, you can submit this request from a hidden iframe. If not, you need to open it in a new window visible to the user. If you need to start the flow with a plain browser redirect (which uses GET), use the legacy `https://api.getmembrane.com/connection-popup` endpoint instead. Add the following parameters to the request (as query parameters or in the body): * token - your tenant's [Access Token](/docs/how-membrane-works/authentication) * requestId - a unique id of this authentication request. It will be used in the callback to separate callbacks from different requests. Note: providing this parameter will result in window being closed after connection is created. If you want it to behave differently (for example, redirect to another URL), you cannot use this parameter. * integrationKey - key of the integration you want to authenticate against. * connectionId (optional) - id of the connection you want to refresh credentials for (if you want to do it for a specific connection) * input (optional) - parameters provided by user in the connection UI, JSON-encoded. ### Custom Callback URL In some cases you need to use a custom Callback URL instead of standard `https://api.getmembrane.com/oauth-callback`. These situations include: * When you need to approve your app and callback URL needs to be on your domain (for example, Google requires that). * You want to create a custom installation logic for some application stores (for example, [Pipedrive app installation flow](https://pipedrive.readme.io/docs/app-installation-flows)). * When you want to do any other custom handling of oAuth callback. You can set custom callback URI in the integration settings. It will be used for all connections created for this integration. Connections illustration After handling the callback URI, you need to redirect user to `https://api.getmembrane.com/oauth-callback` URL with the same query parameters you received in the callback URI. For example, if you use callback url `https://myapp.com/oauth-callback` and a third-party application sends user to the following URL: `https://myapp.com/oauth-callback?code=abcd&state=1234` when you are done with the user, you need to redirect them to: `https://api.getmembrane.com/oauth-callback?code=abcd&state=1234` Here `state` is the value Membrane put in the authorization URL. Forward it exactly as you received it. If the third-party application sends you back no `state`, omit the parameter — do not send `state=null` or an empty value. ### Custom State For OAuth2 connections, you can add a custom component to the `state` parameter used in the callback URI. This can be useful if you want to pass some information through the authentication process. To do this, simply add a `customState` parameter to your POST request to `https://api.getmembrane.com/connect` (or, for the legacy GET flow, to the `https://api.getmembrane.com/connection-popup` URL): ``` POST https://api.getmembrane.com/connect?customState=MY_CUSTOM_STATE ``` When the user is redirected to the callback URI, the `state` parameter will contain the value of `customState` parameter after `:` symbol like this: ``` https://myapp.com/oauth-callback?code=abcd&state=1234:MY_CUSTOM_STATE ``` ### Auth from outside of Membrane When connection is initiated outside of Membrane (for example, from an app store of a third-party application),\ you need to add the following additional query parameters when redirecting user to Membrane: * `token` is [Membrane Token](/docs/how-membrane-works/authentication) you normally use to access our API or SDK. * `integration` is id or key of the integration you want to authenticate against (i.e. `hubspot` or `salesforce`). * `redirectUri` is the URL you want to redirect user to after connection is created. This could be a page in your app you want the user to end up on. In this flow Membrane did not start the authentication, so there is no Membrane `state` to send back. Do not forward the third-party application's own `state` parameter. If your code builds the URL from whatever the application returned, drop `state` when it is missing, so you never send `state=null`. For example, if you use callback url `https://myapp.com/oauth-callback` and a third-party application sends user to the following URL: `https://myapp.com/oauth-callback?code=abcd` when you are done with the user, you need to redirect them to: `https://api.getmembrane.com/oauth-callback?integration=pipedrive&code=abcd&token=AUTH_TOKEN&redirectUri=https://myapp.com/configure-integration` After user's connection is successfully created by Membrane, the user will be redirected to `https://myapp.com/configure-integration` URL.\ If redirectUri is not provided, Membrane will simply close the window after connection is successfully created. If connection creation fails, user will be redirected to your redirectUri with `error` parameter like this: `https://myapp.com/configure-integration?error=ERROR_MESSAGE` ## MCP Connections A connection of `type: 'mcp'` points at an arbitrary [Model Context Protocol](https://modelcontextprotocol.io) server (for example `https://mcp.linear.app/mcp`) instead of a Membrane connector. There is no `integrationKey` and no `connectorId` — the only input is the MCP server URL. When you create one, Membrane performs the full OAuth 2.1 + PKCE handshake against the server (Protected Resource Metadata discovery, Authorization Server discovery, and client registration via CIMD or Dynamic Client Registration), then stores and refreshes the resulting tokens just like any other connection. ### Creating an MCP connection Send a **POST** request to `https://api.getmembrane.com/connect` with `type: 'mcp'` and the server URL in `input.mcpServerUrl`. Because MCP servers require OAuth, open this in a window visible to the user (the same way you would for an `oauth2` connector). The parameters go in the query string or in the `payload` body field (JSON-encoded), the same as every other `/connect` call: ``` POST https://api.getmembrane.com/connect Content-Type: application/x-www-form-urlencoded payload={"type":"mcp","input":{"mcpServerUrl":"https://mcp.linear.app/mcp"}} ``` Include your tenant [Access Token](/docs/how-membrane-works/authentication) (as the `token` parameter or `Authorization` header). `requestId`, `connectionId`, and `customState` behave the same as for connector connections. The user is redirected to the MCP server's authorization page; on approval the connection lands `READY` with encrypted credentials. ### Using MCP tools The MCP server's tools are exposed as **actions** — fetched live from the server on each request, never materialized in the database: * `GET /actions?connectionId=` lists the available tools as action-shaped objects (each carries `mcp: true`; the tool name is the action `key`). * `POST /act` runs a tool: ``` POST https://api.getmembrane.com/act { "connectionId": "", "key": "list_issues", "input": { } } ``` `input` is passed straight through to the MCP tool as its arguments. ### Credentials and refresh MCP connections store OAuth credentials in the encrypted `credentials` field and reuse the same [Token Refresh](#token-refresh) machinery — proactive auto-refresh before expiry (`nextCredentialsRefreshAt`), error-based refresh on a `401`, and the manual `POST /connections/:id/refresh-credentials` endpoint. The server-derived OAuth state (authorization server, registered client, canonical resource) is kept in an internal encrypted field and is not returned with the connection. ## Secrets `credentials`, `input`, and `connectorParameters` are encrypted at rest and are not returned with the connection object by default. To get these values, you should add `includeSecrets=true` to the query parameters of your request to get a single connection.\ This request must be made using workspace admin [access token](/docs/how-membrane-works/authentication). ## Token Refresh Membrane refreshes credentials such as OAuth tokens and sessions both proactively and reactively. **Auto-Refresh** When credentials include an expiration time, Membrane schedules automatic refresh before the token expires. The `nextCredentialsRefreshAt` field shows when the next refresh is scheduled. **Error-Based Refresh** When an API request fails due to an expired token, Membrane automatically attempts to refresh the token and retries the request. If the request fails again after refresh, the connection is marked as disconnected. **Refresh Locking** Membrane uses locking to ensure only one refresh happens at a time for each connection, even when running multiple server instances. **After Refresh** On success, new credentials are stored and the next auto-refresh is scheduled. On failure, the connection is marked as disconnected and auto-reconnect is triggered (if available). ## Auto-Reconnect When a connection is marked as disconnected and the connector has a `test` method implemented, Membrane automatically attempts to reconnect with exponential backoff. **Retry Behavior** Membrane periodically runs the connection test. If the connector can refresh credentials, Membrane refreshes them before each test, so an expired access token does not fail the test. The time between retries increases with each failed attempt. After a maximum number of attempts, retries stop and the connection remains disconnected until manually reconnected. You can view retry status in the `nextRetryTimestamp` and `retryAttempts` fields on the Connection page in the console. **Successful Reconnect** When the connection test passes, either on a retry or when you test the connection manually, the connection is marked as reconnected and all retry data is cleared. **Reconnect after a credentials refresh** If a connection was disconnected because its credentials stopped working, a later successful credentials refresh marks it as reconnected. The refresh can also happen while you use the connection, for example when you run an Action. A connection that was disconnected because its connection test failed stays disconnected until the test passes. ## Connection Lifecycle Webhooks You can subscribe to [webhook notifications](/docs/managing-membrane/monitoring-troubleshooting/webhook-notifications) for connection lifecycle events. The following events are available: * `connection.created` - triggered when a connection is created and credentials are set (if connection is created without credentials, this webhook will not be fired until credentials are set for the first time) * `connection.deleted` - triggered when a connection is archived * `connection.disconnected` - triggered when a connection is marked as disconnected * `connection.reconnected` - triggered when a connection is marked as reconnected # Connectors Source: https://docs.getmembrane.com/reference/workspace-elements/connectors Connectors describe how to authenticate in and interact with a specific [External App](/reference/workspace-elements/external-apps). Example connector definition: ```yaml theme={null} name: Connector Name logoUri: https://static.integration.app/connectors/connector-key/logo.png key: connector-key parametersSchema: type: object properties: parameter: type: string appUuid: 877b0ad6-66b1-4d61-82f9-7e4d62acea69 type: client-credentials # User input configuration inputSchema: type: object # Description shown to users when creating a connection (supports markdown) description: "If you are not sure where to get the information below, follow [this link](https://.example.com/api-authentication)." properties: apiKey: type: string title: "API Key" # Connector functions (see below) # These reference files in your connector directory makeApiClient: type: mapping mapping: baseUri: https://api.example.com headers: Authorization: $concat: - "Bearer " - $var: credentials.api_token test: type: javascript code: | export default async function ({ externalApiClient }) { const responseData = await externalApiClient.get("/current-user") return responseData.email !== undefined } # ... properties and functions based on the connector type ``` Connector definition has the following properties: * `name`: Display name of the connector. * `key`: Unique identifier for the connector. * `appUuid`: UUID of an [External App](/reference/workspace-elements/external-apps). * `logoUri`: URL to the connector's logo (should be publicly available) * `type`: one of the [Connector Types](/reference/workspace-elements/connectors/connector-types). * `parametersSchema`: [Data Schema](/docs/references/data-schemas) for connector parameters (see below) * `inputSchema`: [Data Schema](/docs/references/data-schemas) defining what inputs to request from users when creating a connection. The `description` field can contain markdown and is displayed to users to help them understand where to find the required credentials. * List of [Connector Functions](/reference/workspace-elements/connectors/connector-functions). * `options`: different options for authentication and accessing the external app if supported by the external API. ### Connector Functions Connector has a number of [Membrane Functions](/docs/references/functions) implementing its logic. The composition of these functions is determined by the connector type. ### User Input You can configure what inputs to request from users when they create a connection, and provide helpful guidance on where to find the required credentials. In some cases, like standard OAuth authentication, you don't need to ask users for anything, and you can skip configuring the input schema. The `inputSchema` is a [Data Schema](/docs/references/data-schemas) with these key properties: * `properties` - defines the fields users need to fill in (e.g., API key, subdomain). These values will be copied to the connection parameters and will become connection `credentials` by default (unless you override this behavior). * `description` - help text shown to users explaining where to find the required credentials. Supports markdown, so you can include links to documentation. ### Custom Credentials Schema By default, credentials schema for the connector is auto-calculated based on other parameters (`type`, `inputSchema`, etc). However, when you use `getCredentialsFromConnectionParameters` to transform user input into a different credential structure, you should define `customCredentialsSchema` to describe the actual shape of your credentials. This is useful when: * Your credentials have a different structure than the user input (e.g., generating tokens from API keys) * You need to fetch additional data during connection setup (e.g., user ID, organization info) * You want to store computed values alongside the original input Example: ```yaml theme={null} # Connector definition: Custom credentials with transformed structure type: client-credentials inputSchema: type: object description: 'Enter your API key from the [Settings page](https://example.com/settings).' properties: apiKey: type: string title: 'API Key' # Define the actual structure of credentials after transformation customCredentialsSchema: type: object properties: apiKey: type: string userId: type: string organizationId: type: string # Transform user input into credentials getCredentialsFromConnectionParameters: type: javascript code: | export default async function ({ connectionInput }) { // Fetch additional info using the API key const response = await fetch("https://api.example.com/me", { headers: { "Authorization": `Bearer ${connectionInput.apiKey}` } }) const userInfo = await response.json() return { apiKey: connectionInput.apiKey, userId: userInfo.id, organizationId: userInfo.organizationId } } ``` When `customCredentialsSchema` is defined, it describes the structure that `getCredentialsFromConnectionParameters` returns and what will be stored as the connection credentials. ### Connector Options You can define multiple options for a connector using the `options` field. This allows users to choose between different authentication methods or other parameters related to accessing the external app. Each option has: * `title` - display name for the option * `description` - description of the option * `enabled` - formula to conditionally enable/disable the option (for example, you may want to enable oauth option only if clientId is provided in the connector parameters) * Any connector function or property related to the connector type. All options will inherit functions and properties from the connector definition if not overriden on the option level. If options are defined, the connector itself should not have a `type` property. Example connector definition with options: ```yaml theme={null} # API client definition will be shared across all optoins makeApiClient: type: mapping mapping: baseUri: https://api.example.com headers: Authorization: $concat: - 'Bearer ' - $var: credentials.api_token options: api-token: title: API Token description: Provide your personal API token to connect type: client-credentials inputSchema: type: object description: 'Find your API token in [Account Settings](https://example.com/settings/api).' properties: apiToken: type: string title: 'API Token' oauth: title: OAuth description: Authorize this app to access your account via UI flow. type: oauth2 getOAuthConfig: type: mapping mapping: clientId: $var: connectorParameters.clientId clientSecret: $var: connectorParameters.clientSecret authorizeUri: https://api.example.com/oauth/authorize tokenUri: https://api.example.com/oauth/token ``` ## Connector Parameters If connector logic can be parametrized by developers using the connector, the schema of these parameters should be defined in the connector spec. The most common use of connector parameters is providing clientId/clientSecret for OAuth authentication. Connector parameters will be available in some of the functions used in the connector implementation, mostly in functions related to authentication. You can use connector parameters as variables inside [Formulas](/docs/references/formulas) in the connector definition. Specifically, the following parameters support formulas: * `options.x.enabled` to enable or disable auth options based on the connector parameters. # Connector Functions Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-functions There are a few [Functions](/docs/references/functions) that are common between all connector types: | Method | Description | | - | - | | [makeApiClient](/reference/workspace-elements/connectors/connector-functions/make-api-client) | Configures how your connector makes API requests to the external service, including authentication headers, base URL, and other settings. | | [getCredentialsFromConnectionParameters](/reference/workspace-elements/connectors/connector-functions/get-credentials-from-connection-parameters) | (optional) Transforms user-provided connection parameters into the credential format required for authentication. | | [refreshCredentials](/reference/workspace-elements/connectors/connector-functions/refresh-credentials) | (optional) Defines how to refresh expired or invalid authentication credentials without requiring user intervention. | | [test](/reference/workspace-elements/connectors/connector-functions/test) | (optional, but recommended) Validates if authentication credentials are working correctly by making a test API request. | | [disconnect](/reference/workspace-elements/connectors/connector-functions/disconnect) | (optional) Revokes access tokens or performs cleanup with the external service when a connection is disconnected. | For functions specific to connector type, see a specific article inside [Connector Types](/reference/workspace-elements/connectors/connector-types). # Disconnect Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-functions/disconnect The `disconnect` method defines what happens when a connection is disconnected. Use it to revoke access tokens or perform other cleanup with the external service before credentials are erased from storage. This method is triggered when: * A connection is explicitly disconnected (via the API or the console UI) * A connection is archived This method is optional. If not implemented, the connection will simply have its credentials erased without notifying the external service. ## Implementation **Supported implementation types** * [Javascript](/docs/references/functions/function-types/javascript) ### JavaScript Implementation ```yaml theme={null} # Connector definition: Revoke the access token when disconnecting disconnect: type: javascript code: | export default async function disconnect({ externalApiClient, credentials, }) { await externalApiClient.post('/oauth/revoke', { token: credentials.access_token, }) } ``` ## Input Parameters | Parameter | Description | | - | - | | `connectorParameters` | Parameters configured for the connector in your workspace. Includes client IDs, secrets, and other connector-specific configuration. | | `connectionInput` | Input provided by the user in the connection UI when they set up the connection. | | `credentials` | Current credentials of the connection that will be erased after this method completes. | | `externalApiClient` | API client for making requests to the external service (available if `makeApiClient` is defined on the connector). | ## Return Value The return value is ignored. The method is expected to perform side effects only (e.g., revoking tokens). ## Error Handling If the disconnect method throws an error, the disconnection process will still continue — credentials will be erased and the connection will be marked as disconnected. The error will be logged but will not prevent the disconnection. This ensures that connections can always be cleanly disconnected even if the external service is unavailable. ## Supported Auth Types The disconnect method is available for the following authentication types: * [OAuth2](/reference/workspace-elements/connectors/connector-types/oauth2) * [OAuth1](/reference/workspace-elements/connectors/connector-types/oauth1) * [Client Credentials](/reference/workspace-elements/connectors/connector-types/client-credentials) # Get Credentials from Connection Parameters Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-functions/get-credentials-from-connection-parameters The `getCredentialsFromConnectionParameters` method transforms user-provided connection parameters into the credential format required for authentication.\ This method is useful when the format of credentials needed by the authentication mechanism differs from what users provide during connection setup or when you need to make additional requests to get/refresh credentials. When using this method, you almost always should define the structure of the credentials this method returns in the `customCredentialsSchema` field of the connector definition. ## Implementation This method is implemented in JavaScript: ```yaml theme={null} # Connector definition: Exchange user-provided email/password for an API access token getCredentialsFromConnectionParameters: type: javascript code: | export default async function getCredentialsFromConnectionParameters({ connectionInput, }) { const { email, password } = connectionInput const response = await fetch('https://api.example.com/v1/get-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) const data = await response.json() const accessToken = data?.access_token if (!accessToken) { throw new Error('Failed to get access token') } return { accessToken } } ``` ## Input The method receives a context object with these properties: | Property | Description | | - | - | | `connectionInput` | Parameters provided by the user during connection setup. | | `connectorParameters` | Parameters configured for the connector in your workspace. | ## Return Value The method should return an object containing the credentials that will be added to the `credentials` stored in the [Connection](/docs/how-membrane-works/connections) object. # Make API Client Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-functions/make-api-client The `makeApiClient` method configures how your connector will make API requests to the external service. This method is required for all authentication types and creates a REST API client with the appropriate authentication headers, base URL, and other configuration settings. ## Implementation Examples You can implement this method in two ways: ### Mapping (Recommended) Most API clients can be configured using a mapping: ```yaml theme={null} # Connector definition: Simple Bearer token authentication makeApiClient: type: mapping mapping: baseUri: https://api.example.com headers: Authorization: $concat: - 'Bearer ' - $var: credentials.access_token ``` Available fields in the mapping: * `baseUri` - base URL for all API requests. * `headers` - default headers to include in all requests. * `query` - default query parameters to include in all requests. * `auth` - parameters for basic authentication: * `username` * `password` * `responseHandlers` - list of response handlers to apply to all responses (see below). ### JavaScript Implementation For more complex API clients, you can use JavaScript code. The function should return a RestApiClient configuration object. Example: ```yaml theme={null} # Connector definition: Dynamic API client with custom headers makeApiClient: type: javascript code: | export default function makeApiClient({ credentials }) { return { baseUri: "https://api.example.com", headers: { Authorization: `Bearer ${credentials.access_token}`, "Content-Type": "application/json", Accept: "application/json", }, } } ``` ## Response Handlers By default, API client will handle responses using the following logic: * status >= 200 && status \< 300 - return response. * status == 401 - throw ACCESS\_TOKEN\_EXPIRED error that will trigger an attempt to refresh credentials (if possible). * status == 429 - throw RATE\_LIMIT\_EXCEEDED error that will lead to retrying the request with exponential backoff until timeout is reached. You can augment or override this logic by providing a list of response handlers. Each handler consists of: * A filter that checks response status, headers, or data and returns true if the handler should be applied. * A flag that indicates whether it is a successful request or an error should be thrown. * Data that will be returned if request is successful (by default, the response body will be returned). * Error that will be thrown if the request is not successful. Example: ```yaml theme={null} # Connector definition: Handle custom error response indicating expired token makeApiClient: type: mapping mapping: baseUri: https://api.example.com headers: Authorization: $concat: - 'Bearer ' - $var: credentials.access_token responseHandlers: - match: $eval: $var: response.data.category is: EXPIRED_AUTHENTICATION isSuccess: false error: message: Access token has expired key: access_token_expired data: $var: response.data ``` Handler configuration fields: * `match` - [Formula](/docs/references/formulas) that returns true or false based on the response. * `isSuccess` - flag that indicates whether it is a successful request or an error should be thrown. * `error` - error that will be thrown if the request is not successful. Error fields: * `message` - error message. * `key` - error key (one of the available keys of ConnectionError type) * `data` - additional data to include in the error. # Refresh Credentials Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-functions/refresh-credentials The `refreshCredentials` method defines how to refresh expired or invalid authentication credentials without requiring user intervention. Authentication methods that require refresh by default (like OAuth2) will have standard implementations of credentials refresh. You can use this method to override the standard implementation or add refresh logic to other auth types. ## Implementation **Supported implementation types** * [Javascript](/docs/references/functions/function-types/javascript) ### JavaScript Implementation ```yaml theme={null} # Connector definition: Refresh access token using client credentials refreshCredentials: type: javascript code: | export default async function refreshCredentials({ credentials, connectorParameters, }) { const response = await fetch( `https://api.example.com/v1/generate-token`, { method: "POST", headers: { Authorization: `Basic ${btoa( `${connectorParameters.clientId}:${connectorParameters.clientSecret}` )}`, }, } ) const data = await response.json() const accessToken = data?.access_token if (!accessToken) { throw new Error("Failed to get access token") } return { accessToken } } ``` ## Input Parameters | Parameter | Description | | - | - | | `connectorParameters` | Parameters configured for the connector in your workspace. Includes client IDs, secrets, and other connector-specific configuration. | | `connectionInput` | Input provided by the user in the connection UI when they set up the connection. | | `credentials` | Current credentials of the connection that need to be refreshed. | ## Return Value The method should return an object with the new credentials. These will be merged with the existing credentials. For example, if you only return `{ access_token: "new_token" }`, only the access token will be updated, and other credential fields will remain unchanged. ## Token Expiration Support To enable automatic credential refresh scheduling, include expiration information in your returned credentials: ```javascript theme={null} return { accessToken: 'new_access_token', refreshToken: 'new_refresh_token', expires_in: 3600, // Token expires in 1 hour (3600 seconds) } ``` Or alternatively: ```javascript theme={null} return { accessToken: 'new_access_token', refreshToken: 'new_refresh_token', expiresIn: 7200, // Token expires in 2 hours (7200 seconds) } ``` When expiration information is provided: * Membrane will automatically schedule the next refresh 5 minutes before the token expires * If no expiration info is provided, credentials will be refreshed every 24 hours by default * The minimum interval between refresh attempts is 1 minute # Test Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-functions/test The `test` method verifies if the authentication credentials are valid by making a simple API request to the external service. This method is crucial for validating connections and providing immediate feedback to users during the connection setup process. ## Implementation **Supported implementation types** * [Javascript](/docs/references/functions/function-types/javascript) * [REST API Mapping](/docs/references/functions/function-types/rest-api-mapping) ### JavaScript Implementation The test method is typically implemented in JavaScript: ```yaml theme={null} # Connector definition: Verify credentials by fetching user profile and checking account status test: type: javascript code: | export default async function test({ apiClient }) { try { const responseData = await apiClient.get('/user/profile') if (responseData.accountStatus === 'active') { return true } return false } catch (error) { console.error('Authentication test failed:', error) return false } } ``` ### REST API Mapping Implementation The test method can also be implemented by defining an API request and response mapping: ```yaml theme={null} # Connector definition: Verify credentials by checking if current user endpoint returns an id test: type: rest-api-mapping mapping: path: /current-user method: get responseMapping: $eval: $var: $.response.id isNotEmpty: true ``` ## Input Context The test method receives a context object with these properties: | Property | Description | | - | - | | `connectorParameters` | Parameters configured for the connector in your workspace. | | `connectionInput` | Parameters provided by the user during connection setup. | ## Return Value The test method should return: * `true` or truthy value when the test succeeds * `false` or falsy value when the test fails * Or throw an error with a descriptive message for more detailed failure information ## Best Practices ### Endpoint Selection Choose an endpoint for testing that: 1. **Is Lightweight**: Uses minimal API resources and quota 2. **Requires Authentication**: Actually tests credentials are valid 3. **Has Minimal Permissions**: Works even with restricted access 4. **Is Stable**: Unlikely to change or be deprecated 5. **Returns Quickly**: Provides fast feedback Common test endpoints include: * User profile or account information * API status endpoints * Organization or workspace information * Simple list endpoints with limits (e.g., first 1-2 items) # Connector Types Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-types Connector type determines how you get access to external applications. * [Client Credentials](/reference/workspace-elements/connectors/connector-types/client-credentials) - The simplest authentication type for API keys, basic auth, or custom tokens * [OAuth2](/reference/workspace-elements/connectors/connector-types/oauth2) - Standard protocol for third-party authorization without sharing credentials * [OAuth1](/reference/workspace-elements/connectors/connector-types/oauth1) - Older authorization protocol with signature-based request authentication (rarely used) * [Proxy](/docs/how-membrane-works/connections/api-proxy) - Authenticates with parameters provided by Membrane (so you don't have to register your own OAuth apps). Available only for some applications. * [Membrane Token](/reference/workspace-elements/connectors/connector-types/membrane-token) - Uses Membrane's token system - typically used for internal connectors or services that are aware of Membrane. If not sure which one to pick, go with "Client Credentials" for simple API authentication or "OAuth2" for user-specific access. # Auth Proxy Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-types/auth-proxy Auth Proxy lets you use OAuth credentials provided by Membrane without registering your own OAuth app. You typically don't need to use this authentication type yourself, but you may find it in pre-built connectors. When using Auth Proxy, you will not have access to the connection credentials. Example connector definition: ```yaml theme={null} # Connector definition: Use Membrane-provided OAuth credentials (no custom OAuth app needed) type: auth-proxy proxyKey: auth-proxy-key ``` # Client Credentials Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-types/client-credentials # Client Credentials Authentication Client credentials authentication is the simplest authentication type. It lets you ask for user input, optionally transform it into connection credentials, and use those to access the API. ## Overview This authentication type is ideal for APIs that use: * API keys * Username and password * Basic authentication * Custom token-based authentication that doesn't follow OAuth standards ## Connector Definition Example client credentials connector definition: ```yaml theme={null} # Connector definition: Simple API key authentication with user input form type: client-credentials title: 'API Key Authentication' description: 'Enter your API key to authenticate' # User input form: what to ask from the user ui: schema: type: object properties: apiKey: type: string title: 'API Key' helpUri: 'https://.example.com/api-authentication' # Optional: transform user input into credentials getCredentialsFromConnectionParameters: type: javascript code: | export default async function ({ connectionInput }) { return { apiKey: connectionInput.apiKey } } # Configure API client with credentials makeApiClient: type: mapping mapping: baseUri: https://api.example.com/v1 headers: Authorization: $concat: - 'Bearer ' - $var: credentials.apiKey # Test the connection test: type: javascript code: | export default async function ({ apiClient }) { const user = await apiClient.get("/user") return user.id !== undefined } ``` ## How It Works By default, user input (`connectionInput`) is used directly as connection credentials. If you need to transform this input or add additional information, you can implement the `getCredentialsFromConnectionParameters` method. ## Custom Credential Processing You can implement a custom `getCredentialsFromConnectionParameters` method to transform user input into API credentials: ```yaml theme={null} # Connector definition: Transform user input into credentials with additional fields getCredentialsFromConnectionParameters: type: javascript code: | export default async function ({ connectorParameters, connectionInput, }) { return { apiKey: connectionInput.apiKey, // Add additional fields or transformations authHeader: `Bearer ${connectionInput.apiKey}`, } } ``` This function receives the following input parameters: * `connectorParameters` - parameters configured for the connector in your workspace. * `connectionInput` - input provided by the user in the connection UI. ## API Client Configuration For client credentials authentication, you'll typically need to configure the API client to include the credentials in requests: ```yaml theme={null} # Connector definition: Configure Bearer token authentication makeApiClient: type: mapping mapping: baseUri: https://api.example.com/v1 headers: Authorization: $tpl: template: 'Bearer {token}' values: token: $var: credentials.apiKey Content-Type: application/json ``` ## Common Authentication Patterns ### API Key in Header ```yaml theme={null} # Connector definition: Send API key in X-API-Key header makeApiClient: type: mapping mapping: baseUri: https://api.example.com/v1 headers: X-API-Key: $var: credentials.apiKey Content-Type: application/json ``` ### API Key in Query Parameter ```yaml theme={null} # Connector definition: Send API key as query parameter in every request makeApiClient: type: mapping mapping: baseUri: https://api.example.com/v1 query: api_key: $var: credentials.apiKey headers: Content-Type: application/json ``` ### Basic Authentication ```yaml theme={null} # Connector definition: Basic auth with username/password ui: schema: type: object properties: username: type: string title: 'Username' password: type: string title: 'Password' format: password makeApiClient: type: mapping mapping: baseUri: https://api.example.com/v1 headers: Authorization: $tpl: template: 'Basic {token}' values: token: $jsonata: $base64encode(credentials.username & ':' & credentials.password) Content-Type: application/json ``` # Membrane Token Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-types/membrane-token # Membrane Token Authentication Membrane Token authentication uses Membrane's built-in token system for authentication. This is typically used for internal connectors or services that are aware of Membrane. Membrane Token authentication is useful for: * Services that are specifically designed to work with Membrane * Testing and development scenarios ## Connector Definition Example Membrane Token connector definition: ```yaml theme={null} # Connector definition: Use Membrane's built-in token authentication type: membrane-token ``` With this authentication method, Membrane will add authentication headers to all requests to your API. It will contain a [JSON Web Token](https://jwt.io) signed by the Workspace Secret. The token will contain the following claims: * `iss` - Issuer, will be the key of the workspace integration runs in. * `sub` - Subject, will be the ID of the user who owns the integration being run. * `fields` - Fields of the user who owns the integration being run. ## Headers When using `membrane-token` authentication, Membrane sends **two headers** for backward compatibility: * `X-Membrane-Token` - The new, correctly named header (recommended) * `X-Integration-App-Token` - Legacy header (for backward compatibility) Both headers contain the same JWT token value. You can decode and verify either token to authenticate the request: ```typescript theme={null} import jwt from 'jsonwebtoken' // Use the new header (recommended) const token = headers['x-membrane-token'] || headers['x-integration-app-token'] const workspaceSecret = process.env.WORKSPACE_SECRET // Verify token const decoded = jwt.verify(token, workspaceSecret) // decoded will contain: { iss, sub, fields } ``` ## Migration from Legacy Token If you're migrating from `integration-app-token` authentication: 1. Update your connector definition to use `type: membrane-token` 2. **Optional**: Update your code to read from the `X-Membrane-Token` header instead of `X-Integration-App-Token` 3. The `X-Integration-App-Token` header will continue to be sent for backward compatibility ## Legacy Support For backward compatibility, connectors using the deprecated `integration-app-token` type will continue to work and will only receive the `X-Integration-App-Token` header. # OAuth1 Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-types/oauth1 # OAuth1 Authentication OAuth1 is an older authorization protocol that requires signature-based request authentication. It is rarely used in modern APIs, but some legacy systems still require it. ## Overview OAuth1 authentication provides a secure way for applications to access user data without exposing user credentials. It uses a complex signature-based mechanism to secure each request. ## Connector Definition Example OAuth1 connector definition: ```yaml theme={null} # Connector definition: OAuth1 authentication with signature-based requests type: oauth1 # OAuth1 configuration: consumer credentials and endpoints getOAuthConfig: type: mapping mapping: consumerKey: $var: connectorParameters.consumerKey consumerSecret: $var: connectorParameters.consumerSecret requestTokenUri: https://api.example.com/oauth/request_token authorizeUri: https://api.example.com/oauth/authorize tokenUri: https://api.example.com/oauth/access_token extra: custom_param: value # API client configuration makeApiClient: type: mapping mapping: baseUri: https://api.example.com # Connection test test: type: javascript code: | export default async function ({ apiClient }) { const user = await apiClient.get("/user") return user.id !== undefined } ``` ## getOAuthConfig Returns OAuth1 configuration used to build the authorization URL and token exchange. **Supported implementation types** * [Mapping](/docs/references/functions/function-types/mapping) * [Javascript](/docs/references/functions/function-types/javascript) ### Configuration Parameters | Parameter | Description | | - | - | | `consumerKey` | The OAuth1 consumer key (from connector parameters) | | `consumerSecret` | The OAuth1 consumer secret | | `requestTokenUri` | The endpoint for obtaining a request token | | `authorizeUri` | The endpoint where users are redirected to authenticate | | `tokenUri` | The endpoint for exchanging the request token for an access token | | `extra` | Additional parameters to add to the authorize request | ## OAuth1 Flow The OAuth1 flow in Membrane follows these steps: 1. User initiates authentication 2. Membrane requests a temporary token using the consumer key/secret 3. User is redirected to the service's authorization page with this token 4. User authenticates and grants permissions 5. Service redirects back to Membrane with a verification code 6. Membrane exchanges this code for access tokens 7. The tokens are stored as connection credentials 8. Membrane uses these tokens to make signed API requests # OAuth2 Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/connector-types/oauth2 # OAuth2 Authentication This authentication type implements [OAuth 2](https://oauth.net/2/). It can be configured to work with different variations of the standard. ## Connector Definition Example OAuth2 connector definition: ```yaml theme={null} # Connector definition: Minimal OAuth2 connector with standard authorization code flow type: oauth2 # OAuth configuration: client credentials and endpoints getOAuthConfig: type: mapping mapping: clientId: $var: connectorParameters.clientId clientSecret: $var: connectorParameters.clientSecret authorizeUri: https://auth.example.com/oauth2/authorize tokenUri: https://auth.example.com/oauth2/token # API client: how to make authenticated requests makeApiClient: type: mapping mapping: baseUri: https://api.example.com headers: Authorization: $concat: - 'Bearer ' - $var: credentials.access_token # Connection test: verify credentials are valid test: type: javascript code: | export default async function ({ apiClient }) { const user = await apiClient.get("/user") return user.id !== undefined } ``` ## Authentication Flow The standard OAuth2 authorization code flow: 1. Your product initializes authentication by sending user to the `/connect` endpoint of Membrane engine. 2. User is redirected to `authorizeUri` built using `getOAuthConfig` function. 3. User authenticates at the external app and grants permissions. 4. OAuth provider redirects back to redirectUri generated by `getOAuthConfig` with authorization code. 5. Authorization code is exchanged for access and refresh tokens using `getTokenData` function. 6. (optional) Additional credentials are extracted using `getCredentialsFromAccessTokenResponse` function. 7. Token response as well as additional extracted credentials are stored as connection credentials. 8. Connection is tested using `test` function to determine if it was created successfully. Then, the following things happen: * API requests to the external app are made with a client created by `makeApiClient` function. * When credentials expire, `refreshCredentials` function is called to refresh them. * If you need to extract additional credentials when refreshing them, it is done with `getCredentialsFromRefreshTokenResponse` function. See details of each of the mentioned functions below. ## getOAuthConfig Returns OAuth2 configuration used to build the authorization URL and token exchange. **Supported implementation types** * [Mapping](/docs/references/functions/function-types/mapping) * [Javascript](/docs/references/functions/function-types/javascript) **Arguments** * `connectorParameters` - Connector configuration * `connectionInput` - Connection UI parameters * `redirectUri` - The callback URI to use * `state` - Generated state for the OAuth 2 flow ### Example Implementation ```yaml theme={null} # Connector definition: OAuth configuration: client credentials and endpoints getOAuthConfig: type: mapping mapping: clientId: $var: connectorParameters.clientId clientSecret: $var: connectorParameters.clientSecret authorizeUri: https://auth.example.com/oauth2/authorize tokenUri: https://auth.example.com/oauth2/token scopes: - read - write ``` ### Configuration Parameters | Parameter | Required | Description | | - | - | - | | `clientId` | Yes | OAuth2 client ID | | `clientSecret` | Yes | OAuth2 client secret | | `authorizeUri` | Yes | Authorization endpoint URL | | `tokenUri` | Yes | Token exchange endpoint URL | | `scopes` | No | Array of OAuth scopes to request | | `clientAuthLocation` | No | Where to send credentials: `headers` (default), `body`, or both | | `noRefreshToken` | No | Set `true` if API doesn't return refresh tokens | | `skipPkce` | No | Set `true` to disable PKCE (enabled by default) | | `extra` | No | Additional query parameters for the authorize URI | ### Authorize URI Formation The authorize URI is built by taking your `authorizeUri` and adding these parameters: * `client_id` - from your config * `redirect_uri` - defaults to `{MEMBRANE_API_URI}/oauth-callback` * `response_type=code` * `access_type=offline` - to request refresh tokens (set `access_type` to `null` in `extra` parameters to remove it) * `scope` - your scopes joined with spaces * `state` - as identifier of the current authentication flow * PKCE parameters (unless `skipPkce: true`): * `code_challenge` * `code_challenge_method=S256` * Parameters from `extra` **Redirect URI** The redirect URI defaults to `{BASE_URI}/oauth-callback` where `BASE_URI` is your Membrane instance URL. To override, set `oAuthCallbackUri` on the integration to use a custom callback URL. ## getTokenData Exchanges the authorization code for access and refresh tokens. **Supported implementation types** * [Javascript](/docs/references/functions/function-types/javascript) **Arguments** * `connectorParameters` - Connector configuration * `connectionInput` - Connection UI parameters * `codeVerifier` - PKCE code verifier * `queryParameters` - Query params from OAuth callback * `redirectUri` - The redirect URI used **Default Implementation** If not implemented, makes a POST request to `tokenUri` with: Body: * `grant_type=authorization_code` * `code` - authorization code from callback * `redirect_uri` - same URI used in authorize step * Client credentials (based on `clientAuthLocation`): * `headers` (default): Basic Authorization header with base64-encoded `clientId:clientSecret` * `body`: `client_id` and `client_secret` in request body * PKCE parameters (unless `skipPkce: true`): * `code_verifier` * `code_challenge_method=S256` Headers: * `Content-Type: application/x-www-form-urlencoded` The response must include `access_token`. If `noRefreshToken: false` (default), `refresh_token` is also required. **Example Implementation** ```javascript theme={null} // Connector definition: getTokenData implementation getTokenData: type: javascript code: | export default async function ({ connectorParameters, connectionInput, codeVerifier, queryParameters, redirectUri }) { const response = await fetch(connectorParameters.tokenUri, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', code: queryParameters.code, redirect_uri: redirectUri, client_id: connectorParameters.clientId, client_secret: connectorParameters.clientSecret, code_verifier: codeVerifier, // Add custom parameters resource: 'https://api.example.com', }), }); return await response.json(); } ``` ## getCredentialsFromAccessTokenResponse Transforms the token response into connection credentials. **Supported implementation types** * [Mapping](/docs/references/functions/function-types/mapping) * [Javascript](/docs/references/functions/function-types/javascript) **Arguments** * `connectorParameters` - Connector configuration * `connectionInput` - Connection UI parameters * `queryParameters` - Query params from OAuth callback * `tokenResponse` - Raw response from token exchange **Default Implementation** If not implemented, the raw token response is stored as credentials. **Example Implementation** Use when you need to: * Extract specific fields from token response * Make additional API calls to fetch user info * Transform or normalize credential structure * Add computed fields (e.g., token expiration timestamp) ```javascript theme={null} // Connector definition: getCredentialsFromAccessTokenResponse implementation getCredentialsFromAccessTokenResponse: type: javascript code: | export default async function ({ connectorParameters, connectionInput, queryParameters, tokenResponse, }) { // Make additional API calls if needed const userInfoResponse = await fetch('https://api.example.com/userinfo', { headers: { Authorization: `Bearer ${tokenResponse.access_token}`, }, }); const userInfo = await userInfoResponse.json(); // Return transformed credentials return { access_token: tokenResponse.access_token, refresh_token: tokenResponse.refresh_token, // Add custom fields userId: userInfo.id, userName: userInfo.name, expires_at: Date.now() + tokenResponse.expires_in * 1000, }; } ``` ## getCredentialsFromRefreshTokenResponse Transforms the refresh token response into updated credentials. **Supported implementation types** * [Javascript](/docs/references/functions/function-types/javascript) **Arguments** * `connectorParameters` - Connector configuration * `connectionInput` - Connection UI parameters * `credentials` - Current connection credentials * `tokenResponse` - Raw response from refresh token request **Default Implementation** If not implemented, the raw refresh response is merged with existing credentials. **Example Implementation** Use when you need to: * Transform refresh token response structure * Calculate token expiration from `expires_in` * Preserve existing credential fields ```javascript theme={null} // Connector definition: getCredentialsFromRefreshTokenResponse implementation getCredentialsFromRefreshTokenResponse: type: javascript code: | export default async function ({ connectorParameters, connectionInput, credentials, tokenResponse, }) { return { access_token: tokenResponse.access_token, // Preserve refresh_token if not returned refresh_token: tokenResponse.refresh_token || credentials.refresh_token, expires_at: Date.now() + tokenResponse.expires_in * 1000, }; } ``` ## makeApiClient Creates an API client configuration using the connection credentials. **Supported implementation types** * [Mapping](/docs/references/functions/function-types/mapping) * [Javascript](/docs/references/functions/function-types/javascript) **Arguments** * `credentials` - Connection credentials **Example Implementation** ```yaml theme={null} # File: auth/make-api-client.map.yml args: baseUri: https://api.example.com headers: Authorization: $concat: values: - Bearer - $var: credentials.access_token delimiter: ' ' Accept: application/json ``` See [makeApiClient](/reference/workspace-elements/connectors/connector-functions/make-api-client) for more details. ## refreshCredentials Refreshes expired access tokens using the refresh token. **Supported implementation types** * [Javascript](/docs/references/functions/function-types/javascript) **Arguments** * `connectorParameters` - Connector configuration * `connectionInput` - Connection UI parameters * `credentials` - Current connection credentials (including `refresh_token`) **Default Implementation** If not implemented, makes a POST request to `tokenUri` with: Body (x-www-form-urlencoded): * `grant_type=refresh_token` * `refresh_token` - from connection credentials * `client_id` - from config * `client_secret` - from config Headers: * `Content-Type: application/x-www-form-urlencoded` Returns updated credentials that are merged with existing credentials. **Example Implementation** Implement when the API uses non-standard refresh token flow: ```javascript theme={null} // Connector definition: refreshCredentials implementation refreshCredentials: type: javascript code: | export default async function ({ connectorParameters, connectionInput, credentials, }) { const response = await fetch(connectorParameters.tokenUri, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${btoa( `${connectorParameters.clientId}:${connectorParameters.clientSecret}` )}`, }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: credentials.refresh_token, // Add custom parameters scope: 'read write', }), }); const refreshResponse = await response.json(); // Return updated credentials (merged with existing) return { access_token: refreshResponse.access_token, refresh_token: refreshResponse.refresh_token || credentials.refresh_token, expires_at: Date.now() + refreshResponse.expires_in * 1000, }; } ``` See [refreshCredentials](/reference/workspace-elements/connectors/connector-functions/refresh-credentials) for more details. ## Common Pitfalls ### Missing refresh token configuration Some APIs don't return refresh tokens (they use long-lived access tokens instead). Check the API's token endpoint documentation for the response format. If refresh token is not explicitly included in the response, set `noRefreshToken: true` in `getOAuthConfig`, otherwise connection creation will fail with a missing refresh token error. # Universal Data Models Source: https://docs.getmembrane.com/reference/workspace-elements/connectors/universal-data-models Universal Data Models provided by connector map its collections to [Universal Data Models](/reference/workspace-elements/connectors/universal-data-models) and back. They are stored in `udm.yml` file in the connector. Example: ```yaml theme={null} contacts: # Universal data model collectionMappings: # How it maps to data collections in connector - key: contacts # Collection it maps to fieldsFromCollection: # Mapping from collection fields to UDM (using formulas) id: $var: $.id fullName: $concat: values: - $var: $.firstName - $var: $.lastName delimiter: ' ' firstName: $var: $.firstName lastName: $var: $.lastName # ... more mappings fieldsToCollection: # Mapping from UDM fields to collection fields id: $var: $.id firstName: $var: $.firstName lastName: $var: $.lastName # ... more mappings ``` # Data Collections Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections Data collections allow working with data in an external application using a consistent API. They let you do the following: * Get schema of data record fields, including custom fields configured for a particular connection. * Perform CRUD operations on data records using a consistent API. * Understand which operations are possible and not possible for a given collection. * Subscribe to data change events even if webhooks are not supported by the underlying API. To learn how to work with data collections, see [Data Collections API Reference](/reference/workspace-elements/data-collections)
Data Collections are connector-level interfaces. To change them, you need to modify the [Connector](/reference/workspace-elements/connectors) that implements a given data collection. ## Spec The data collection specification defines the structure and capabilities of a data collection. It is located in the root of the data collection directory (`data//spec.yml`). Here's an example: ```yaml theme={null} contacts: name: Contacts fieldsSchema: type: object properties: id: type: string readOnly: true email: type: string firstName: type: string lastName: type: string fullName: type: string writeOnly: true methods: list: filterFields: - email create: requiredFields: - email update: excludedFields: - id delete: {} findById: {} match: fields: - email search: {} events: created: implementationType: webhook updated: implementationType: pull-latest-records deleted: implementationType: custom-pull ``` ## Properties | Property | Type | Description | | - | - | - | | `name` | string | Display name for the collection | | `parametersSchema` | object | [Data Schema](/docs/references/data-schemas) for collection-level parameters | | `fieldsSchema` | object | [Data Schema](/docs/references/data-schemas) defining the structure of records in the collection | | `methods` | object | Method implementations. See below for details. | | `events` | object | Event implementations. See below for details. | ### Parameters Data collection parameters are used to configure the data collection implementation. They are passed to execution context of all data collection functions as `parameters` argument. You should only add parameters to the data collection when you cannot use function-level parameters. If collection-level and function-level parameters overlap, function-level parameters take precedence. ## Fields Schema This [Data Schema](/docs/references/data-schemas) defines fields of the records in this data collection. It is recommended that all the fields that could participate in any operation of this collection be present in the fields schema. If some fields are only available in read or write operations, you can mark them as `readOnly` or `writeOnly`. Furthermore, you can configure which fields are available for which operations (i.e. `create` or `update`) in the corresponding operation configuration. **Custom Fields** If a data collection supports custom fields that can be configured on a per-connection basis, you can implement a `customFields` function that returns the custom fields schema.\\ The custom fields schema will be merged with `fieldsSchema` when returning the collection's specification. ## Methods You can configure the following methods for the data collection: | Method | Description | | - | - | | [`list`](/reference/workspace-elements/data-collections/data-collection-methods/data-collection-list) | Read all the records from the collection (with optional filters). | | [`findById`](/reference/workspace-elements/data-collections/data-collection-methods/data-collection-find-by-id) | Get one record from the collection using its id. | | [`match`](/reference/workspace-elements/data-collections/data-collection-methods/data-collection-match) | Find a single matching record in a data collection using provided fields (e.g., by email). | | [`search`](/reference/workspace-elements/data-collections/data-collection-methods/data-collection-search) | Find a list of records using a string query. Supports type-ahead search whenever possible. | | [`create`](/reference/workspace-elements/data-collections/data-collection-methods/data-collection-create) | Create one record in a data collection. | | [`update`](/reference/workspace-elements/data-collections/data-collection-methods/data-collection-update) | Update a record in a data collection using its id. | | [`delete`](/reference/workspace-elements/data-collections/data-collection-methods/data-collection-delete) | Delete a record with a given id. | ## Data Collection Functions These are non-method functions that affect the data collection functionality. | Function | Description | | - | - | | [`recordFromFields`](/reference/workspace-elements/data-collections/data-collection-record-from-fields) | Build Data Records from their fields - every data collection must have one | | [`spec`](/reference/workspace-elements/data-collections/data-collection-dynamic-spec) | Dynamically generate the collection specification based on API capabilities | | [`customFields`](/reference/workspace-elements/data-collections/custom-fields) | Dynamically generate custom fields schema based on API configuration | | [`fieldsFromApi`](/reference/workspace-elements/data-collections/data-collection-fields-from-api) | Transform API response fields to match your fieldsSchema format | | [`fieldsToApi`](/reference/workspace-elements/data-collections/data-collection-fields-to-api) | Transform fieldsSchema format to API format before sending requests | When you want to transform fields that are coming from the API before returning them from the connector, or transform fields that come from the client before sending them to the API, you can use `fieldsFromApi` and `fieldsToApi` functions respectively. These functions will be automatically applied to fields for all method and event implementations. `fieldsToApi` will be automatically applied after method is executed and before sending the request to the API. `fieldsFromApi` will be automatically applied after the method is executed and before the result is returned to the client. This could be useful if: * Fields in the API are inconsistent between endpoints. * Field structures in the API are too complex and you want to simplify them. ## Inheritance (Mixins) When multiple data collections share the implementation of some of their functions, you can use mixins. Example: ```yaml theme={null} # spec.yml name: Users extends: - key: _webhooks parameters: objectKey: user ``` If a collection uses a mixin, all the functions (and methods) from the mixin will be used as the collection's functions unless overridden. If a mixin collection has parameters, each collection that uses the mixin can provide different parameter values. # Custom Fields Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/custom-fields Many applications let users create custom object fields. When a [Data Collection](/reference/workspace-elements/data-collections) supports custom fields, it will have: * `customFields` set to `true` in its specification. * `fieldsSchema` include both standard and custom fields. In most cases you should simply work with `fieldsSchema` and not worry about doing anything special about custom fields. ## See also Representing data structures used in integrations. Configuring access to data in external apps. # Dynamic Specification Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-dynamic-spec By defining `spec` function, you can dynamically generate the collection specification. This is useful when the collection's capabilities depend on API features, user permissions, or configuration parameters. ## Implementation ```javascript theme={null} module.exports = async ({ spec, credentials, parameters }) => { // Check API capabilities const capabilities = await makeApiRequest({ method: 'GET', path: '/api/capabilities', credentials, }) // Modify spec based on capabilities if (capabilities.supportsSearch) { spec.search = { parametersSchema: { type: 'object', properties: { query: { type: 'string' }, }, }, } } // Add dynamic filter fields if (parameters.enableAdvancedFilters) { spec.list.filterFields = ['email', 'firstName', 'lastName', 'createdAt'] } return spec } ``` ## Input | Property | Type | Description | | - | - | - | | `spec` | object | The base collection specification | | `credentials` | object | Connection credentials | | `parameters` | object | Collection parameters | ## Return Value Returns the modified collection specification. ## Example ```javascript theme={null} // Base spec: { "name": "Contacts", "fieldsSchema": { /* ... */ }, "list": { "filterFields": ["email"] }, "create": {}, "update": {} } // spec method returns: { "name": "Contacts", "fieldsSchema": { /* ... */ }, "list": { "filterFields": ["email", "firstName", "lastName", "createdAt"] }, "create": {}, "update": {}, "search": { "parametersSchema": { "type": "object", "properties": { "query": { "type": "string" } } } } } ``` # Events Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-events Data collections provide events for when records are created, updated, or deleted. ## Event Types * `created` - Triggered when a new record is created * `updated` - Triggered when an existing record is updated * `deleted` - Triggered when a record is deleted ## Implementation Types | Implementation Type | Description | When to Use | Documentation | | - | - | - | - | | **Webhook** ⭐ | Real-time event notifications via HTTP callbacks | External app supports webhooks | [Webhook Events](/reference/workspace-elements/external-events/webhook) | | **Pull Latest Records** | Fetch records based on timestamps | API supports timestamp filtering, no webhooks | [Pull Latest Implementation](/reference/workspace-elements/data-collections/data-collection-events/data-collection-events-pull-latest)s | | **Custom Pull** | Subscribe/pull with state management | API has delta/change tracking | [Custom Pull Implementation](/reference/workspace-elements/data-collections/data-collection-events/data-collection-events-custom-pull) | | **Full Scan** | Compare complete record snapshots | No other options available, small collections only | [Full Scan Implementation](/reference/workspace-elements/data-collections/data-collection-events/data-collection-events-full-scan) | ⭐ **Recommended** - Choose webhook implementation whenever possible for best performance and real-time updates. If event implementation is not provided, a `full-scan` implementation will be used by default. ## Data Collection Events Configuration Example ```yaml theme={null} events: created: implementationType: webhook # or pull-latest, custom-pull, full-scan updated: implementationType: webhook deleted: implementationType: webhook ```
## Configuring Intervals The [custom pull](/reference/workspace-elements/data-collections/data-collection-events/data-collection-events-pull-latest) and [pull-latest records](/reference/workspace-elements/data-collections/data-collection-events/data-collection-events-pull-latest) implementation types use a default interval of **300 seconds**, while **Full scan** uses a default interval of **10,800 seconds**. These intervals can be configured at either the **trigger node level** (when a data source is not used) or the **data source level**(when a data source is used) using the **pull update interval** and **full sync interval** options. Refer to the [limits page](/docs/managing-membrane/limits#external-event-pulls) to see the minimum allowed interval. Events illustration # Custom Pull Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-events/data-collection-events-custom-pull Custom Pull Strategy provides a customized approach for retrieving lists of created, updated, or deleted records. This strategy consists of two methods: `subscribe` and `pull`. ## How It Works 1. When a subscription is created, a `state` is generated and stored. It allows pulling events starting from the subscription time. 2. When events are pulled, the `state` is used to generate events since the state was created, as well as the new state for the next pull. ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name events: created: implementationType: custom-pull methods: subscribe: implementationType: javascript filePath: events/custom-subscribe.js pull: implementationType: javascript filePath: events/custom-pull.js updated: implementationType: custom-pull methods: subscribe: implementationType: javascript filePath: events/custom-subscribe.js pull: implementationType: javascript filePath: events/custom-pull.js deleted: implementationType: custom-pull methods: subscribe: implementationType: javascript filePath: events/custom-subscribe.js pull: implementationType: javascript filePath: events/custom-pull.js ``` ## Functions ### subscribe **File:** `-subscribe.`, for example `created-subscribe.js` The subscribe method initializes the subscription to events and retrieves a unique identifier for future pulls. **Arguments:** * `parameters` – object, parameters (matching `parametersSchema`) of this particular data collection. **Result:** * `state` – object, subscription state. This state will be saved and passed as `state` to the `pull` function. **Example:** ```javascript theme={null} // Example: Subscribe to delta changes and return the deltaLink. export default async function subscribe({ apiClient, parameters }) { let cursor = false const path = `${parameters.path}/delta` while (true) { const response = await apiClient.get(`${cursor ? cursor : path}`) if (response.deltaLink) { break } else { cursor = response.nextPageLink } } return { state: { deltaLink: response.deltaLink } } } ``` ### pull **File:** `-pull.`, for example `created-pull.js` The pull method fetches records based on the subscription and stores them in the state.\ It also generates events for each created, updated, or deleted record. **Arguments:** * `state` – object, state returned from `subscribe` function. * `parameters` – object, parameters (matching `parametersSchema`) of this particular data collection. **Result:** * `events` – list of objects representing events with `type` and `record` fields. * `state` – updated state information, including the new information for subsequent pulls. **Example:** ```javascript theme={null} // Example: Pull records using the deltaLink and generate events. export default async function pull({ apiClient, state, parameters }) { const events = [] let cursor = false while (true) { const response = await apiClient.get(`${cursor ? cursor : state?.deltaLink}`) response.value.forEach((record) => { events.push({ type: 'created', record: record, }) }) if (response.deltaLink) { break } else { cursor = response.nextPageLink } } return { events, state: { deltaLink: response.deltaLink, }, } } ``` # Full Scan Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-events/data-collection-events-full-scan This is the "last resort" implementation type. It will be used if no other implementation type is provided. The full scan strategy fetches all records from the external app and compares them with the previously fetched records to determine the changes. ## How It Works 1. Fetches all records from the collection 2. Compares current records with previously stored records 3. Detects created, updated, and deleted records by comparing snapshots 4. Generates appropriate events for each change ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name events: created: implementationType: full-scan updated: implementationType: full-scan ignoredFields: - lastViewedAt - downloadUrl - temporaryToken deleted: implementationType: full-scan ``` ## Parameters * `ignoredFields` – list of fields that should be ignored when comparing records. For example, dynamically generated fields such as download URLs or last view timestamps need to be ignored to avoid false updates. ## Limitations Due to the need to read the full collection and compare the full list of records between previous and current read, it can become quite resource-intensive. To avoid unexpected excessive usage of resources (worker time, API calls), we limit the full scan event pulls with: * **100,000 records** in a collection (to avoid running out of memory) * **1,000 pages** of records in a collection (to avoid excessive use of API requests) * **20 minutes** of runtime of pulling all collection records (to avoid unexpected cost associated with long-running background tasks) Whenever one of these limits is reached, the full sync pull will fail. ## When to Use * **Last resort only** - when no other implementation type is available * Small to medium-sized collections (under 100k records) * APIs without webhook or change tracking support * Simple data structures without frequent updates ## Not Recommended For * Large collections (>100k records) * Frequently updated data * APIs with rate limiting * Production systems with high performance requirements ## Alternative Recommendations If you expect your collections to be larger than the limits, we recommend implementing periodic full re-sync of the whole collection into your database instead of relying on events generated by the full scan. ## Best Practices * Use `ignoredFields` to exclude dynamic or frequently changing fields # Pull Latest Records Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-events/data-collection-events-pull-latest The "pull latest records" strategy involves fetching records based on their creation, update, or deletion date. Use this strategy when the external app does not support webhook events. ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name events: created: implementationType: pull-latest methods: pullLatestRecords: implementationType: javascript ``` ## Functions There is only one method: `pullLatestRecords`. ### Pull Latest Records **File:** `-pull-latest-records.`, for example `created-pull-latest-records.js` **Arguments:** * `cursor` – string, cursor returned from the previous page of the results. If not provided, the first page is returned. * `parameters` – object, parameters (matching `parametersSchema`) of this particular data collection. **Result:** * `records` – list of objects representing records, ordered by creation date or date-time in descending order. * `cursor` – string (optional) providing information to retrieve the next chunk of records if pagination is allowed. **Example Implementation:** ```javascript theme={null} export default async function pullLatestRecords({ apiClient, cursor, parameters }) { // Define the path based on your External App's API structure. const path = `${parameters.path}?$orderby=createdDateTime desc` // Make the API request to retrieve records. const response = await apiClient.get(`${cursor ? cursor : path}`) // Return the result with records and optional cursor for pagination. return { records: response.records, cursor: response.nextPageLink, } } ``` # Webhook Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-events/data-collection-events-webhook This is the preferred implementation type for data events whenever available. **File:** Webhook implementations are configured in the connector's events configuration and use the same webhook endpoints as connector-level events. See details of how webhooks are implemented here: [Webhook Events](/reference/workspace-elements/external-events/webhook). Webhook implementation for data collections is exactly the same as for connector-level webhook events. ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name events: created: implementationType: webhook updated: implementationType: webhook deleted: implementationType: webhook ``` ## Implementaiton Data collection webhook events are implemented in the same way as the normal webhook events. See [Webhook Events](/reference/workspace-elements/external-events/webhook) for detailed implementation. Implementation files are located in the data collection folder and named as `-.`, for example `created-webhook-subscribe.js` or `created-webhook-handle.map.yml`. # Fields From API Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-fields-from-api The `fieldsFromApi` method transforms raw API responses into the format defined by your collection's `fieldsSchema`. This method is called automatically when retrieving records from the external API. ## Implementation Examples ### Mapping Implementation (Recommended) **File:** `fields-from-api.map.yml` ```yaml theme={null} id: { $var: fields.contact_id } email: { $var: fields.email_address } firstName: { $var: fields.first_name } lastName: { $var: fields.last_name } fullName: $join: [' ', [{ $var: fields.first_name }, { $var: fields.last_name }]] createdAt: $cond: - { $var: parameters.includeTimestamps } - { $var: fields.created_at } - null updatedAt: $cond: - { $var: parameters.includeTimestamps } - { $var: fields.updated_at } - null ``` ### JavaScript Implementation **File:** `fields-from-api.js` ```javascript theme={null} export default async function fieldsFromApi({ fields, parameters }) { // Transform API response to match fieldsSchema const transformed = { id: fields.contact_id, email: fields.email_address, firstName: fields.first_name, lastName: fields.last_name, // Add computed fields fullName: `${fields.first_name} ${fields.last_name}`, } // Add optional fields based on parameters if (parameters?.includeTimestamps) { transformed.createdAt = fields.created_at transformed.updatedAt = fields.updated_at } return transformed } ``` ## Input | Property | Type | Description | | - | - | - | | `fields` | object | Raw fields from the API response | | `parameters` | object | Collection parameters | ## Return Value The method should return an object matching the collection's `fieldsSchema`. ## Example ```javascript theme={null} // API returns: { "contact_id": "123", "email_address": "john@example.com", "first_name": "John", "last_name": "Doe", "created_at": "2024-01-01T00:00:00Z" } // fieldsFromApi transforms to: { "id": "123", "email": "john@example.com", "firstName": "John", "lastName": "Doe", "fullName": "John Doe" } ``` # Fields To API Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-fields-to-api The `fieldsToApi` method transforms data from your `fieldsSchema` format into the format required by the external API. This method is called automatically before sending data in create or update operations. ## Implementation Examples ### Mapping Implementation (Recommended) **File:** `fields-to-api.map.yml` ```yaml theme={null} email_address: { $var: fields.email } first_name: { $var: fields.firstName } last_name: { $var: fields.lastName } contact_id: | { $cond: [ { $var: fields.id }, { $var: fields.id }, null ] } custom_fields: | { $cond: [ { $and: [ { $var: parameters.includeCustomFields }, { $var: fields.customData } ]}, { $var: fields.customData }, null ] } ``` ### JavaScript Implementation **File:** `fields-to-api.js` ```javascript theme={null} export default async function fieldsToApi({ fields, parameters }) { // Transform fieldsSchema to API format const apiFields = { email_address: fields.email, first_name: fields.firstName, last_name: fields.lastName, } // Only include ID for updates, not creates if (fields.id) { apiFields.contact_id = fields.id } // Add custom field mapping if enabled if (parameters?.includeCustomFields && fields.customData) { apiFields.custom_fields = fields.customData } return apiFields } ``` ## Input | Property | Type | Description | | - | - | - | | `fields` | object | Fields in your fieldsSchema format | | `parameters` | object | Collection parameters passed | ## Return Value The method should return an object in the format expected by the external API. ## Example ```javascript theme={null} // Your fields: { "id": "123", "email": "john@example.com", "firstName": "John", "lastName": "Doe", "fullName": "John Doe" } // fieldsToApi transforms to: { "contact_id": "123", "email_address": "john@example.com", "first_name": "John", "last_name": "Doe" } ``` # Methods Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-methods # Create Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-methods/data-collection-create The `create` method adds a new record to the collection. ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name methods: create: fields: - email - firstName - lastName requiredFields: - email excludedFields: - id parametersSchema: type: object properties: sendWelcomeEmail: type: boolean default: true ``` Properties: * **`fields`** (array): Whitelist of fields to include * **`requiredFields`** (array): Fields that must be provided * **`excludedFields`** (array): Fields to exclude from creation ## Implementation Examples ### REST API Mapping Implementation (Recommended) **File:** `create.rest.yml` ```yaml theme={null} path: /contacts method: POST requestMapping: data: email: { $var: $.fields.email } first_name: { $var: $.fields.firstName } last_name: { $var: $.fields.lastName } phone: { $var: $.fields.phone } send_welcome_email: { $var: $.parameters.sendWelcomeEmail } responseMapping: id: { $string: { $var: response.data.contact_id } } ``` ### Operation Mapping Implementation **File:** `create.yml` ```yaml theme={null} implementationType: operation-mapping mapping: operationKey: createContact inputMapping: contactData: { $var: $.fields } options: sendWelcomeEmail: { $var: $.parameters.sendWelcomeEmail } outputMapping: id: { $string: { $var: result.contactId } } ``` ### JavaScript Implementation **File:** `create.js` ```javascript theme={null} export default async function create({ apiClient, fields, parameters }) { const requestBody = { email: fields.email, first_name: fields.firstName, last_name: fields.lastName, phone: fields.phone, send_welcome_email: parameters?.sendWelcomeEmail || false, } const response = await apiClient.post('/contacts', requestBody) return { id: String(response.data.contact_id), } } ``` ## Input | Property | Type | Description | | - | - | - | | `fields` | object | Record fields in API format (after fieldsToApi) | | `credentials` | object | Authentication credentials | | `parameters` | object | Method-specific parameters | ## Return Value | Property | Type | Description | | - | - | - | | `id` | string | The ID of the created record | # Delete Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-methods/data-collection-delete The `delete` method removes a record from the collection. ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name methods: delete: parametersSchema: type: object properties: hardDelete: type: boolean default: false description: Permanently delete instead of soft delete ``` ## Implementation Examples ### REST API Mapping Implementation (Recommended) **File:** `delete.rest.yml` ```yaml theme={null} path: /contacts/{id} method: DELETE requestMapping: pathParameters: id: { $var: $.id } query: permanent: { $var: $.parameters.hardDelete } responseMapping: success: true ``` ### JavaScript Implementation **File:** `delete.js` ```javascript theme={null} export default async function deleteRecord({ apiClient, id, parameters }) { const queryParams = {} if (parameters?.hardDelete) { queryParams.permanent = true } await apiClient.delete(`/contacts/${id}`, { params: queryParams }) return { success: true, } } ``` ## Input | Property | Type | Description | | - | - | - | | `id` | string | The ID of the record to delete | | `credentials` | object | Authentication credentials | | `parameters` | object | Method-specific parameters | # Find By ID Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-methods/data-collection-find-by-id The `findById` method retrieves a single record by its unique identifier. ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name methods: findById: parametersSchema: type: object properties: includeRelated: type: boolean default: false description: Include related records ``` ## Implementation Examples ### REST API Mapping Implementation (Recommended) **File:** `find-by-id.rest.yml` ```yaml theme={null} path: /contacts/{id} method: GET requestMapping: pathParameters: id: { $var: $.id } query: expand: | { $cond: [ { $var: $.parameters.includeRelated }, "relationships", null ] } responseMapping: record: { $var: response.data } ``` ### Operation Mapping Implementation **File:** `find-by-id.yml` ```yaml theme={null} implementationType: operation-mapping mapping: operationKey: getContact inputMapping: contactId: { $var: $.id } options: includeRelated: { $var: $.parameters.includeRelated } outputMapping: record: { $var: result.contact } ``` ### JavaScript Implementation **File:** `find-by-id.js` ```javascript theme={null} export default async function findById({ apiClient, id, parameters }) { try { const queryParams = {} if (parameters?.includeRelated) { queryParams.expand = 'relationships' } const responseData = await apiClient.get(`/contacts/${id}`, { params: queryParams, }) return { record: responseData.record, } } catch (error) { if (error.response?.status === 404) { return { record: null, } } throw error } } ``` ## Input | Property | Type | Description | | - | - | - | | `id` | string | The ID of the record to retrieve | | `credentials` | object | Authentication credentials | | `parameters` | object | Method-specific parameters | ## Return Value | Property | Type | Description | | - | - | - | | `record` | object | The record in API format (before fieldsFromApi) | # List Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-methods/data-collection-list The `list` method retrieves multiple records from a collection with support for filtering and pagination. ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name methods: list: filterFields: - email - firstName - lastName parametersSchema: type: object properties: includeArchived: type: boolean default: false ``` Properties: * **`parametersSchema`** (DataSchema): Schema for the additional parameters * **`filterFields`** (array): Fields that can be used for filtering of the results. ## Implementation Examples ### REST API Mapping Implementation (Recommended) **File:** `list.rest.yml` ```yaml theme={null} path: /contacts method: GET requestMapping: query: limit: 100 offset: { $var: $.cursor } email: { $var: $.filter.email } first_name: { $var: $.filter.firstName } last_name: { $var: $.filter.lastName } include_archived: { $var: $.parameters.includeArchived } responseMapping: records: { $var: response.data.contacts } cursor: | { $cond: [ { $var: response.data.hasMore }, { $string: { $sum: [{ $number: { $var: $.cursor } }, 100] } }, null ] } ``` ### JavaScript Implementation **File:** `list.js` ```javascript theme={null} export default async function list({ apiClient, filter, cursor, parameters }) { const queryParams = { limit: 100, offset: cursor || 0, includeArchived: parameters?.includeArchived || false, } // Apply filters if (filter?.email) { queryParams.email = filter.email } if (filter?.firstName) { queryParams.first_name = filter.firstName } if (filter?.lastName) { queryParams.last_name = filter.lastName } const response = await apiClient.get('/contacts', { params: queryParams }) return { records: response.data.contacts, cursor: response.data.hasMore ? String(queryParams.offset + 100) : undefined, } } ``` ## Input | Property | Type | Description | | - | - | - | | `filter` | object | Filter criteria based on configured filterFields | | `cursor` | string | Pagination cursor from previous request | | `credentials` | object | Authentication credentials | | `parameters` | object | Method-specific parameters | ## Return Value | Property | Type | Description | | - | - | - | | `records` | array | Array of records in API format (before fieldsFromApi) | | `cursor` | string | Cursor for next page (optional) | | `drilldowns` | array | Additional requests to fetch related data (optional) | ## Drilldowns The list method supports drilldowns for fetching nested or related data. This is useful for scenarios like fetching files from multiple folders or tasks across different projects. ```javascript theme={null} // Example: Fetching files from folders module.exports = async ({ filter, cursor, parameters }) => { if (!cursor && filter?.parentFolderId === 'root') { // First, get all folders const folders = await makeApiRequest({ method: 'GET', path: '/api/folders', credentials, }) // Return folders with drilldowns for their files return { records: folders.data, drilldowns: folders.data.map((folder) => ({ parameters: { recursive: true }, filter: { type: 'file', parentFolderId: folder.id, }, })), } } // Regular file listing const response = await makeApiRequest({ method: 'GET', path: '/api/files', query: { folderId: filter?.parentFolderId }, credentials, }) return { records: response.data, } } ``` ### Drilldown Structure ```json theme={null} { "drilldowns": [ { "parameters": { "recursive": true }, "filter": { "type": "file", "parentFolderId": "123" } } ] } ``` # Match Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-methods/data-collection-match The `match` method finds a single record by matching specific field values. This is useful for finding records by unique fields other than the ID. ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name methods: match: fields: - email - externalId parametersSchema: type: object properties: caseSensitive: type: boolean default: true ``` Configuration properties: * **`parametersSchema`** (DataSchema): Schema for the additional parameters * **`fields`** (array): Fields that can be used for matching ## Implementation Examples ```javascript theme={null} module.exports = async ({ fields, credentials, parameters }) => { const queryParams = {} // Build query from match fields if (fields.email) { queryParams.email = fields.email } const response = await makeApiRequest({ method: 'GET', path: '/api/contacts', query: queryParams, credentials, }) // Return first matching record return { record: response.data.length > 0 ? response.data[0] : null, } } ``` ## Input | Property | Type | Description | | - | - | - | | `fields` | object | Field values to match against | | `credentials` | object | Authentication credentials | | `parameters` | object | Method-specific parameters | ## Return Value | Property | Type | Description | | - | - | - | | `record` | object\|null | The matching record or null if not found | ## Example ```javascript theme={null} // Input: { "fields": { "email": "john@example.com" } } // Returns: { "record": { "contact_id": "123", "email_address": "john@example.com", "first_name": "John", "last_name": "Doe" } } ``` ## Notes * Return `record: null` if no match is found * The record is automatically processed through `fieldsFromApi` * Use this for deduplication or finding records by natural keys * Only configured fields in the `fields` array can be used for matching # Search Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-methods/data-collection-search The `search` method performs a text-based search across records in the collection. ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name methods: search: parametersSchema: type: object properties: searchFields: type: array items: type: string enum: ['name', 'email', 'phone'] default: ['name', 'email'] description: Fields to search in ``` ## Implementation Examples ```javascript theme={null} module.exports = async ({ query, cursor, credentials, parameters }) => { const response = await makeApiRequest({ method: 'GET', path: '/api/contacts/search', query: { q: query, limit: 50, offset: cursor || 0, }, credentials, }) return { records: response.data, cursor: response.hasMore ? String((cursor || 0) + 50) : undefined, } } ``` ## Input | Property | Type | Description | | - | - | - | | `query` | string | The search query string | | `cursor` | string | Pagination cursor from previous request | | `credentials` | object | Authentication credentials | | `parameters` | object | Method-specific parameters | ## Return Value | Property | Type | Description | | - | - | - | | `records` | array | Array of matching records | | `cursor` | string | Cursor for next page (optional) | # Update Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-methods/data-collection-update The `update` method modifies an existing record in the collection. ## Configuration Example ```yaml theme={null} # spec.yml name: Collection Name methods: update: fields: - email - firstName - lastName excludedFields: - id - createdAt parametersSchema: type: object properties: partial: type: boolean default: true description: Allow partial updates ``` ### Configuration Options * **`fields`** (array): Whitelist of fields that can be updated * **`excludedFields`** (array): Fields that cannot be updated ## Implementation Examples ### REST API Mapping Implementation (Recommended) **File:** `update.rest.yml` ```yaml theme={null} path: /contacts/{id} method: PUT requestMapping: pathParameters: id: { $var: $.id } body: { $var: $.fields } responseMapping: id: | { $or: [ { $string: { $var: response.data.contact_id } }, { $var: $.id } ] } ``` ### Operation Mapping Implementation **File:** `update.yml` ```yaml theme={null} implementationType: operation-mapping mapping: operationKey: updateContact inputMapping: contactId: { $var: $.id } contactData: { $var: $.fields } updateOptions: partial: { $var: $.parameters.partial } outputMapping: id: { $string: { $var: result.contactId } } ``` ### JavaScript Implementation **File:** `update.js` ```javascript theme={null} export default async function update({ externalApiClient, id, fields, parameters }) { const method = parameters?.partial ? 'PATCH' : 'PUT' const responseData = await externalApiClient.makeApiRequest({ method: method, path: `/contacts/${id}`, data: fields, }) return { id: String(responseData.contact_id || id), } } ``` ## Input | Property | Type | Description | | - | - | - | | `id` | string | The ID of the record to update | | `fields` | object | Updated fields in API format (after fieldsToApi) | | `credentials` | object | Authentication credentials | | `parameters` | object | Method-specific parameters | ## Return Value | Property | Type | Description | | - | - | - | | `id` | string | The ID of the updated record | # Record From Fields Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-collection-record-from-fields The `recordFromFields` method builds a complete record object from field data. This allows you to add metadata or compute additional properties beyond the basic field values. ## Implementation ```javascript theme={null} module.exports = async ({ fields, parameters }) => { return { id: fields.id, fields: fields, // Add metadata raw: { source: 'api', version: '2.0', }, // Compute display name name: `${fields.firstName} ${fields.lastName}`, // Add URL to view record url: `https://app.example.com/contacts/${fields.id}`, } } ``` ## Input | Property | Type | Description | | - | - | - | | `fields` | object | Processed fields (after fieldsFromApi transformation) | | `parameters` | object | Collection parameters passed from the connection | ## Return Value Returns a record object with the following structure: | Property | Type | Description | | - | - | - | | `id` | string | Unique identifier for the record (required) | | `fields` | object | The field data | | `name` | string | Display name for the record (optional) | | `raw` | object | Raw data or metadata (optional) | | `url` | string | URL to view the record in the external app (optional) | ## Example ```javascript theme={null} // Input fields: { "id": "123", "email": "john@example.com", "firstName": "John", "lastName": "Doe" } // recordFromFields returns: { "id": "123", "fields": { "id": "123", "email": "john@example.com", "firstName": "John", "lastName": "Doe" }, "name": "John Doe", "url": "https://app.example.com/contacts/123", "raw": { "source": "api", "version": "2.0" } } ``` ## Notes * The `id` field is required and must be a string * The `fields` property should contain all the field data * Use `name` to provide a human-readable identifier for the record * The `url` property enables users to navigate to the record in the external application # Data Records Source: https://docs.getmembrane.com/reference/workspace-elements/data-collections/data-records Each record in Data Collections will have the following structure: ```yaml theme={null} id: name: url: fields: field: ... ``` Connectors are responsible for transforming data from application-specific format into Data Records and back. Each record will have the following top-level properties: * `id` – record id, unique within a given data collection. * `name` – human-readable name of the record. It will be a person's name in case of person, task title in case of task, etc. * `uri` – link to the record if it has its own page. * `createdTime` * `updatedTime` * `deletedTime` * `createdById` – id of an entity (usually a user) that created the record. * `updatedById` – id of an entity that updated the record last. * `fields` – data fields of the record that match `fieldsSchema` of the collection. Usually they match exactly the fields returned by the external application. They may differ if connector developer decided to simplify the schema for end-users. Connector will try to do its best to populate as many of these properties as possible. At the very least, `id` is always populated. # Data Links Source: https://docs.getmembrane.com/reference/workspace-elements/data-links Data Links represent relations between data records in your application and data records in external applications. For example, if you want to link a User in your app with a corresponding User in a third-party app, Data Links are what you need. ## Data Link Tables Data Links are organized in Data Link Tables. You can manage the list of tables on the [Data Links](/reference/workspace-elements/data-links) page. You should create a Data Link Table for each type of relation you want to manage, like "Users to Users" or "Meetings to Deals". ## Using Data Links You can use Data Links in: * [Create Data Link](/reference/workspace-elements/flows/function-nodes) and [Find Data Link](/reference/workspace-elements/flows/function-nodes) flow nodes. * Find Data Link [Formula](/docs/references/formulas) * Via API. ## Deployments See [Data Link Table Instances](/reference/workspace-elements/data-links/data-link-table-instances) # Data Link Table Instances Source: https://docs.getmembrane.com/reference/workspace-elements/data-links/data-link-table-instances Each pair of Data Link Table and [Connection](/docs/how-membrane-works/connections) will have a separate Data Link Table Instance. You can find all the Data Link Table Instances on the [Instances](https://console.integration.app/w/0/deployments/data-link-table-instances) page.\ If you click on a given Data Link Instance, you can see and modify the list of Data Links in that instance. Whenever you create a link from one record to another, it is recorded in a corresponding Data Link Table Instance. ## Data Links A Data Link is a record within a Data Link Table Instance that has: * `externalRecordId` – id of a record on the external app's side. * `appRecordId` – id of a record on your app's side. * `direction` – one of: * `export` – link from your app to an external app. * `import` – link from external app to your app. ### Creating Data Link A Data Link is created with the following data: * `dataLinkTableInstanceId` – id of Data Link Table Instance associated with the record * `externalRecordId` * `appRecordId` * `direction` – `export`, `import`, or `both`. If direction `both` is selected then both `export` and `import` links will be created. > Only one link with the same `externalRecordId`, `appRecordId`, and `direction` can exist within a single Data Link Table Instance. If another one is created, it will overwrite the previously existing one. ### Finding Data Link When finding a data link with a Flow Node or a Formula, you need to provide: * `dataLinkTableInstanceId` * `direction` * `recordId` Find External Record Id and Find App Record Id formulas provide `direction` automatically. # Data Sources Source: https://docs.getmembrane.com/reference/workspace-elements/data-sources Data Sources work on top of [Data Collections](/reference/workspace-elements/data-collections) and provide advanced functionality, such as: * Ability for your tenants to choose which data collection to use and configure its parameters. * Share these settings between different actions and flows: they will be updated in all places simultaneously. * Provide dynamic schemas for Field Mappings. ## Universal and Integration-specific Data Sources When you want to work with similar data in multiple applications, create a universal data source. Data Sources illustration They let you use the same code and logic for any app you integrate with and can be used in universal [Flows](/reference/workspace-elements/flows) and [Field Mappings](/reference/workspace-elements/field-mappings). For example, "Users" or "Tasks" will most often be universal data sources. In most cases, you would select a [Universal Data Model](/reference/workspace-elements/connectors/universal-data-models) for a Data Source to make it automatically map to the right data in each external application. If you want to work with data that is specific to a particular application, create an integration-specific data source. For example, "Slack Canvases" or "JIRA Workflows" may be integration-specific data sources. You can customize universal data sources for each individual application after you create them. It lets you work with data that is similar, but not exactly the same in each app. ## Tenant Instances To actually interact with data, you need to create a Data Source Instance for a specific [Connection](/docs/how-membrane-works/connections). Through a Data Source Instance, you can navigate data, get information about the collection, read and write data, and react to events. See more detail in the [Data Source Instances](/reference/workspace-elements/data-sources/data-source-instances) article. ## Navigating Data Source Depending on an application, there may be different ways of getting data, for example: * Application may have multiple similar objects (Task and Subtasks, or Leads and Contacts). * There could be multiple lists of objects user may want to select. * There could be custom objects user may use for a given integration. Data Sources illustration To deal with this, you can request a list of available data locations from a Data Source Instance and then set its `path` field to point to a specific collection. See [Navigating Data Source Instance](/reference/workspace-elements/data-sources/data-source-instances) article for details. ## Data Collection Specification When a Data Source Instance is pointing to a collection, you can use its `collectionSpec` property to get information about the collection: * How do records look like (their fields schema with names, types, descriptions, and possible values). * What operations are supported (create, update, delete, etc). * What methods of receiving events are supported (real-time push, polling, etc.). For more detail, see the [Data Collection](/reference/workspace-elements/data-collections) article. ## Reading and Writing Data See the [API](/reference/workspace-elements/data-sources/data-source-instances) docs for more detail. # Data Source Instances Source: https://docs.getmembrane.com/reference/workspace-elements/data-sources/data-source-instances To interact with a Data Source, you need to get a Data Source Instance for a specific [Connection](/docs/how-membrane-works/connections). You can get an instance by Data Source Identifier (key or id) and Connection Identifier (integration key or connection id): ```javascript theme={null} const dataSourceInstance = await membrane .connection('hubspot') .dataSource('{DATA_SOURCE_KEY}') .get({ autoCreate: true }) ```
Data Source Instance has the following properties: | Property | Description | | - | - | | `id` | Unique identifier of the instance. | | `path` | Path to the Data Collection the data source is pointing to. | | `defaultPath` | Path to the Data Collection the data source is pointing to by default. | | `udm` | A Universal Data Model records in this data source will be transformed to / from. | | `pullUpdatesIntervalSeconds` | How often the data source will be pulled for new events. | | `fullSyncIntervalSeconds` | How often the data source will be fully synced (to get events that require a full sync). | | `subscriptions` | Information about active subscriptions to Data Source Events (see below for details). | | `isCustomized` | Whether this data source was customized by your tenant (path was changed, etc.) | ## Advanced Options Data Sources can be configured with the following advanced options: * Pull Updates Interval – number of seconds between each pull for updates such as new events (except events that require Full Sync). Setting it lower will increase the number of API requests and may result in hitting rate limits. * Full Sync Interval – number of seconds between full syncs of the data source. Full Sync is a very heavy operation since it pulls ALL the data from the data source. It generates events for subscriptions that require Full Sync. ## API Here is how to interact with data in external applications using [Data Sources](/reference/workspace-elements/data-sources). ### Create ```javascript theme={null} await membrane.connection('hubspot').dataSource('{DATA_SOURCE_KEY}').create() ``` ### Get ```javascript theme={null} const dataSourceInstance = await membrane .connection('hubspot') .dataSource('{DATA_SOURCE_KEY}') .get({ autoCreate: true }) ```
### Update ```javascript theme={null} await membrane.connection('hubspot').dataSource('{DATA_SOURCE_KEY}').patch('{INPUT}') ```
### Setup or Refresh ```javascript theme={null} await membrane.connection('hubspot').dataSource('{DATA_SOURCE_KEY}').setup() ```
### Reset ```javascript theme={null} await membrane.connection('hubspot').dataSource('{DATA_SOURCE_KEY}').reset() ```
### Archive ```javascript theme={null} await membrane.connection('hubspot').dataSource('{DATA_SOURCE_KEY}').archive() ```
## Navigating Data Source Instances Data Sources point to [Data Collections](/reference/workspace-elements/data-collections) in external apps.\ They let you store and customize data collection and their parameters per tenant. This page describes how to navigate data sources and configure them for your tenants. Data Source Instances illustration ### List Data Collections To get the list of available data collections in a given connector: ```javascript theme={null} await membrane.integration('hubspot').getDataCollections() ```
The response will contain a `locations` property with a list of nested directories and collections.\ If there are too many to return at once, the response will also contain a `cursor` property. You have to pass it back to get the next page of results. To list locations inside a specific directory, pass its `path` to the `getLocations` method. ### Point Data Source to a Collection When a Data Source is created, it will point to a default collection (if one exists in the external app). You can change it by updating the `collectionKey` property of the Data Source: ```javascript theme={null} const dataSourceInstance = await membrane.connection('hubspot').dataSource('{DATA_SOURCE_KEY}').patch({ collectionKey: '{COLLECTION_KEY}', collectionParameters: '{COLLECTION_PARAMETERS}', }) ```
You can only update the `collectionKey` to point to a Collection. After you update `collectionKey`, accessing the data source's `collection` uses the new path. ### Data Collection Parameters Some collections have parameters. For example, a collection "Tasks by Project" may have a `projectId` parameter. If a collection has parameters, it will have a `parametersSchema` property.\ It is a [Data Schema](/docs/references/data-schemas) for its parameters. To apply parameters to a data collection, add them as a query string to the `path` property of the Data Source Instance, like this: ```json theme={null} { "path": "/data/tasks?projectId=123" } ``` ## Working with Data You can read and write data records using a Data Collection identified by the `collectionKey` property of the Data Source Instance. ## Multiple Data Source Instances per Connection By default, each pair of Connection and Data Source will have only one Data Source instance.\ If you change it (for example, set a `collectionKey`), it will apply to every place you use a given Data Source with a given connection. In some cases you may want to create multiple Data Source Instances for the same Connection and Data Source—\ for example, if you have multiple locations you want to import. This can be achieved by providing `instanceKey` when accessing a Data Source. Each `instanceKey` will create a new Data Source Instance. ```javascript theme={null} await membrane .connection('hubspot') .dataSource('{DATA_SOURCE_KEY}', { instanceKey: '{INSTANCE_KEY}' }) .get({ autoCreate: true }) ``` # External API Logs Source: https://docs.getmembrane.com/reference/workspace-elements/external-api-logs # Apps Source: https://docs.getmembrane.com/reference/workspace-elements/external-apps You can add, configure, and remove your integrations with external apps from the [Apps](/reference/workspace-elements/connectors) section of the console. When adding apps, you can choose from a list of supported integrations we have in our library or write in your own. Apps illustration
## Connector To interact with an external app, it needs a [connector](/reference/workspace-elements/connectors). For popular apps, you can usually find a connector in our library of public connectors.\ For less popular apps, you can create a custom connector using [Connector Builder](/reference/workspace-elements/connectors) or ask our Solutions team to do this for you (with a predictable cost and time). ### Configuring Connectors Some connectors may require you to configure them. In those cases, there will be configuration documentation you can follow: Apps illustration ### Connector Versions All connectors are versioned. A connector will never be updated without your consent. You can rely on the connector logic to be unchanged as long as the version stays the same. If there is a new version of a connector available, you will see a notification and will be able to update it in one click: Apps illustration You can also switch to any other connector version by clicking on the current version number. Connectors follow semantic versioning scheme, which means: * If the last digit of the version is changed, it is a patch update. It usually includes bug fixes or minor improvements. It should be safe to update. * If the middle digit is changed, it is a minor update. It usually includes new features or changes that are backward-compatible. It may include breaking changes, so you should read the changelog to see if it's safe to update. * If the first digit is changed, it is a major update. It usually includes breaking changes. You should review the changes and test the integration before updating, or consider creating a new integration with the new connector version and deprecating the current one. ### Switching Connectors You can switch a connector used for this app to a different one. The most common scenario is switching between a public connector and a custom connector you've created. Apps illustration # External Event Logs Source: https://docs.getmembrane.com/reference/workspace-elements/external-event-logs # External Event Pulls Source: https://docs.getmembrane.com/reference/workspace-elements/external-event-pulls # External Events Source: https://docs.getmembrane.com/reference/workspace-elements/external-events External events let you get notified about things that happen in an external application. External Events are connector-level interfaces. To change them, you need to modify the [Connector](/reference/workspace-elements/connectors) that implements a given data collection. As an alternative, you can define stand-alone [External Event Types](/reference/workspace-elements/external-events/external-event-types) directly in your workspace — useful when you want to subscribe to events without modifying (or even thinking about) a connector. This feature is currently experimental. Each event type has: * Name * Parameters (optional) * Payload schema Events are located in the `events` folder of the connector. Each event has its own folder named `events/` (for example, `events/user-created`). ## Event Specification The event specification is located in the `spec.yml` file in the event folder. It has the following format: ```yaml theme={null} name: Task Created implementationType: webhook parametersSchema: type: object properties: projectId: type: string description: ID of the project to monitor for new tasks required: - projectId schema: type: object properties: id: type: string title: type: string description: type: string status: type: string methods: subscribe: implementationType: javascript unsubscribe: implementationType: javascript ``` Let's break it down: | Property | Description | | - | - | | `name` | Human-readable name of the event that will be shown in the UI | | `implementationType` | Type of event implementation. Can be `webhook` or `global-webhook` | | `parametersSchema` | JSON Schema describing parameters that need to be provided when subscribing to the event. In the example above, we require a `projectId` to know which project to monitor | | `schema` | JSON Schema describing the event payload structure that will be received when the event occurs | | `methods` | Object describing implementation methods for the event. The list of methods depends on the `type` of the event. | ## Implementation Types ### Webhook This event implementation type uses webhook subscriptions to get events. When an event subscription is created, it registers a webhook and handles incoming payloads. When an event subscription is deleted, it unsubscribes from the webhook. It optionally supports a refresh mechanism to keep the subscription active if the external app requires it. Full article: [Webhooks](/reference/workspace-elements/external-events/webhook) ### Global Webhook This event implementation type uses [Global Webhooks](/reference/workspace-elements/external-events/global-webhook) to get events. Full article: [Global Webhook](/reference/workspace-elements/external-events/global-webhook) # External Event Subscriptions Source: https://docs.getmembrane.com/reference/workspace-elements/external-events/external-event-subscriptions External event subscriptions are created when a [Flow](/reference/workspace-elements/flows) has a trigger that reacts to external events and the flow is enabled. Subscription is automatically disabled (and un-subscribed) when there are no more flows that need it. You can find the list of subscriptions and information about each of them here: [External Event Subscriptions](https://console.integration.app/w/0/deployments/external-event-subscriptions). Integration engine automatically de-duplicates subscriptions. If multiple elements need the same subscription, we create only one subscription and share it between them. For example, if multiple flows are subscribed to updates of data records in the same collection, only one subscription will be created. ## Automatic Resubscription When you update the parameters of a data source instance (for example, changing collection parameters in the list view), Membrane automatically: 1. Unsubscribes from the existing external event subscription 2. Creates a new external event subscription based on the updated parameters This ensures your external event subscriptions always stay in sync with your data source configurations without requiring manual updates. # External Event Types Source: https://docs.getmembrane.com/reference/workspace-elements/external-events/external-event-types External Event Types are an **experimental** workspace-level element. The shape of the API and console UI may change before it is generally available. External Event Types let you subscribe to events from an external app **without modifying a [Connector](/reference/workspace-elements/connectors)** — or thinking about connectors at all. Compared to connector-level [External Events](/reference/workspace-elements/external-events), an External Event Type is a stand-alone workspace element with its own JavaScript implementation. You define the event in your workspace, point it at an [Integration](/reference/workspace-elements/integrations) or a specific [Connection](/reference/workspace-elements/connections), and Membrane runs the same `subscribe` / `handle` / `unsubscribe` lifecycle it runs for connector events. Use a stand-alone External Event Type when: * The connector you use is shared (for example a public connector from the Membrane Universe) and you don't want to fork it just to add one event. * Different tenants need different events from the same app and managing that variability inside the connector would be awkward. * You want to iterate on event handling without going through a connector publish cycle. External Event Types **cannot be used in Flow triggers**. The `connector-event-trigger` node only resolves event keys defined inside a [Connector](/reference/workspace-elements/connectors). To consume events emitted by a stand-alone External Event Type, create an [External Event Subscription](/reference/workspace-elements/external-events/external-event-subscriptions) and point it at a webhook URI you control — see [Consuming events](#consuming-events) below. ## Scoping A single External Event Type can live at one of three scopes: | Scope | Set when… | Applies to | | - | - | - | | **Universal** | No `integrationId` or `connectionId` is set | Every integration in the workspace. Children can be created per integration to override the definition. | | **Integration** | `integrationId` is set | All connections of a single integration. | | **Connection** | `connectionId` is set | A single connection (typically used to override the type for one tenant). | This mirrors how stand-alone [Actions](/reference/workspace-elements/actions) are scoped — the same "universal → integration → connection" override chain applies. ## Definition External Event Types are managed from the **External Event Types** page in the console (under the experimental section), or via the `/external-event-types` API endpoints. Each type has: | Field | Description | | - | - | | `name` | Human-readable name shown in the console. | | `key` | Stable identifier used in API calls and exports. | | `type` | Implementation flavour. Currently only `webhook` is supported, so `subscribe`, `unsubscribe`, and `handle` must all be defined. | | `parametersSchema` | JSON Schema for parameters the subscriber must supply (for example, `projectId`). | | `schema` | JSON Schema describing the payload of emitted events. | | `example` | Optional example payload, used for testing and to generate `schema`. | | `subscribe` | **Required.** JavaScript function called once when a subscription is created. Registers the webhook with the external system and returns `state`. | | `handle` | **Required.** JavaScript function called for each incoming webhook payload. Returns `{ events, response?, state? }`. | | `unsubscribe` | **Required.** JavaScript function called when the subscription is deleted. Deregisters the webhook. | | `refresh` | Optional JavaScript function called periodically to keep the subscription alive when the external app requires it. | The four function slots have the same signatures and semantics as the corresponding connector-level methods documented in [Webhook Events](/reference/workspace-elements/external-events/webhook) — the only difference is where the definition lives. ### Example implementation A minimal end-to-end example for an external app that exposes a `/webhooks` REST endpoint. The `subscribe` function registers the webhook and stashes the returned id; `handle` extracts events from the incoming request; `unsubscribe` tears the registration down; `refresh` (optional) extends the registration on a schedule. ```javascript subscribe.js theme={null} export default async function subscribe({ apiClient, webhookUri, parameters }) { const { data } = await apiClient.post('/webhooks', { url: webhookUri, event: parameters.eventName, }) return { state: { webhookId: data.id }, // Optional — only set if the external app expires webhooks. Omit to disable refresh(). nextRefreshTimestamp: Date.now() + 24 * 60 * 60 * 1000, } } ``` ```javascript handle.js theme={null} export default async function handle({ data }) { // `data` is the parsed body of the incoming webhook request. return { events: [ { type: data.action, // e.g. 'created', 'updated', 'deleted' record: data.payload, }, ], } } ``` ```javascript unsubscribe.js theme={null} export default async function unsubscribe({ apiClient, state }) { await apiClient.delete(`/webhooks/${state.webhookId}`) } ``` ```javascript refresh.js theme={null} export default async function refresh({ apiClient, state }) { await apiClient.post(`/webhooks/${state.webhookId}/refresh`) return { nextRefreshTimestamp: Date.now() + 24 * 60 * 60 * 1000, } } ``` See [Webhook Events](/reference/workspace-elements/external-events/webhook) for the full argument and return shapes (signature verification, response shaping, state updates from `handle`, and so on). ## Consuming events Stand-alone External Event Types are not wired to Flow triggers. Instead, you create an [External Event Subscription](/reference/workspace-elements/external-events/external-event-subscriptions) yourself and tell Membrane where to forward incoming events. ### 1. Create the subscription `POST /external-event-subscriptions` with the connection and event type you want to subscribe to: ```bash theme={null} curl -X POST https://api.getmembrane.com/external-event-subscriptions \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "651b…", "externalEventTypeId": "67ab…" }' ``` Membrane immediately runs the event type's `subscribe` function against the connection. The response includes the new subscription's `id` and its initial `status` (`subscribed` on success). ### 2. Attach a webhook URI Subscriptions created this way do **not** have a destination yet — incoming events are stored and logged but go nowhere until you set `webhookUri`: ```bash theme={null} curl -X PATCH https://api.getmembrane.com/external-event-subscriptions/{id} \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhookUri": "https://your-app.example.com/membrane/events" }' ``` From this point on, each event returned by the type's `handle` function is POSTed to `webhookUri` with the event payload as the request body. The URI must be a public `http://` or `https://` URL — Membrane rejects non-HTTP(S) schemes, loopback, link-local, and other private addresses at delivery time. You can change or clear the destination later by `PATCH`-ing `webhookUri` again (use `""` to unset). To stop receiving events entirely, `DELETE` the subscription; Membrane will run the event type's `unsubscribe` function before removing the record. ## Logs and Troubleshooting Stand-alone External Event Types use the same monitoring surfaces as connector-level events: * [External Webhooks](/docs/managing-membrane/monitoring-troubleshooting/logs/external-webhooks) — raw incoming payloads * [External Events](/docs/managing-membrane/monitoring-troubleshooting/logs/external-events) — processed events and their status * [External Event Subscriptions](/reference/workspace-elements/external-events/external-event-subscriptions) — subscription state and history # Global Webhook Events Source: https://docs.getmembrane.com/reference/workspace-elements/external-events/global-webhook This method of collecting events is used when an [External App](/reference/workspace-elements/external-apps) uses a single webhook per connected application rather than allows creating any number of connection-layer webhooks. In this case, the single webhook should be configured by the developer of the connected application and events received through this webhook need to be routed to a correct [Tenant](/docs/how-membrane-works/tenants) and [Connection](/docs/how-membrane-works/connections) but the Global Webhooks mechanism. This mechanism consists of two parts: * Global Webhook definition inside a [Connector](/reference/workspace-elements/connectors). * External Event definition with `global-webhook` implementation type. When a connector with global webhooks is used in an [Integration](/reference/workspace-elements/integrations), a unique global webhook URI is generated for each global webhook defined in the connector. You can find the corresponding webhook URIs by using [Get integration webhooks](/api-reference/integrations/list-integration-global-webhooks) API endpoint. ## Global Webhook Definition Global webhooks are defined inside a [Connector](/reference/workspace-elements/connectors). They are defined as connector files inside `global-webhooks` directory, for example: ``` // Connector files global-webhooks/webhook-key/spec.yml // Global Webhok definition global-webhooks/webhook-key/handle.js // Implementation of the "handle" function. ``` The developer needs to take this URL and provide it to the external app as a webhook (usually as part of the OAuth application configuration).\ You should provide details on how to do this in the connector documentation. ### spec.yml Global webhook specification looks like this: ``` name: Webhook Name key: webhook-key methods: handle: implementationType: javascript ```
### handle The handle function is called when data is sent to the webhook. Example: ``` export default async function handle({ data }) { return { events: [ { payload: data, selector: `user_id:${data.from_user_id}`, }, ], response: { status: 201, data: "Webhook received" } } } ``` **Arguments:** * `query` – object, webhook request query received from the external app * `data` – object, webhook request body received from the external app (parsed based on Content-Type) * `headers` – object, webhook request headers received from the external app * `parameters` – connector parameters configured on the integration the webhook belongs to * `rawBody` – string, raw unparsed body of the webhook request (useful for signature verification or when you need access to the original request body) **Result:** * `events` – list of events. Each event has the following properties: * `selector` (string) – selector to find matching external event subscriptions (see [External Events](/docs/managing-membrane/monitoring-troubleshooting/logs/external-events)) * `payload` – arbitrary event payload that will be passed to matched subscriptions. * `response` – object, optional HTTP response with properties: * `headers` – object * `status` – number * `data` – HTTP body (based on Content-Type) ## Global Webhook Event [External Events](/reference/workspace-elements/external-event-logs) implemented through global webhook look like this: ``` // events/event-key/spec.yml name: Event Name implementationType: global-webhook key: event-key methods: get-event-selector: implementationType: javascript ``` ### Functions The following functions can be implemented for events with `global-webhook` implementation type: **getEventsSelector** This method returns data that will be used to route the event to the correct connection. Supported implementation types: `javascript`, `mapping` Example: ``` export default async function getEventSelector({ apiClient }) { const response = await apiClient.get('users/me') return { globalWebhookKey: 'global-webhok-key', globalWebhookEventSelector: `user_id:${response?.id}`, } } ``` Function arguments: * `parameters` – object of parameters (matching `parametersSchema`) of this particular event. Return value is an object with the following fields: * `globalWebhookKey` – key of the global webhook defined in the connector (see [Global Webhooks](/reference/workspace-elements/external-events/global-webhook)) * `globalWebhookEventSelector` – string that is used to route the event to the correct subscription. This string typically needs to be generated dynamically based on the event parameters and information we can get from the current connection (using apiClient or credentials). When a global webhook handles a payload and returns events, it will be routed to a given [External Event Subscriptions](/reference/workspace-elements/external-events/external-event-subscriptions) if: * `globalWebhookKey` returned from this method matches the key of the global webhook * `globalWebhookEventSelector` returned from this method matches the event selector associated to the event returned by the `handle` method of the global webhook. # Webhook Events Source: https://docs.getmembrane.com/reference/workspace-elements/external-events/webhook This type of event subscribes to a webhook and then receives event notifications. ## Methods The following methods can be implemented for webhook events: ### Subscribe * **Method key:** `subscribe` * **Supported implementation types:** `javascript`, `rest-api-mapping`, `graphql-api-mapping` This function subscribes to events in the external app. **Arguments:** * `webhookUri` – string, URI webhooks should be sent to. It should be passed to the external app. * `parameters` – object, parameters (matching `parametersSchema`) of this particular event. **Result:** * `state` – object, subscription state. This state will be saved and passed as `state` to `unsubscribe` and `handle` functions. * `nextRefreshTimestamp` – number, optional, timestamp in milliseconds when the next refresh should be performed. Used to refresh the subscription if the external app requires it. Example: ```javascript theme={null} // Example: Subscribe to a webhook and return the webhookId. export default async function subscribe({ apiClient, webhookUri, parameters }) { const body = { webhookUri, '': '' } const response = await apiClient.post(parameters.path, body) return { state: { webhookId: response?.id, }, } } ``` ### Handle * **Method key:** `handle` * **Supported implementation types:** `javascript`, `mapping` This function processes notification requests from the external app.\ If the external app requires a response, you can specify parameters (`headers`, `data`) for the response in the `response` property (optional). This can be useful if the external API requires confirmation of the webhook URI. **Arguments:** * `query` – object, webhook request query received from the external app * `data` – object, webhook request body received from the external app (parsed based on Content-Type) * `headers` – object, webhook request headers received from the external app * `state` – object, state returned from `subscribe` function * `parameters` – object, parameters (matching `parametersSchema`) of this particular event * `rawBody` – string, raw unparsed body of the webhook request (useful for signature verification or when you need access to the original request body) **Result:** * `events` – list of objects, list of events extracted from the webhook. Each `event` is an object with `type` and `record` fields. * `response` – object, optional HTTP response with properties: * `headers` – object * `status` – number * `data` – HTTP body (based on Content-Type) * `state` – a new value of the state if it needs to be updated. Example result: ```json theme={null} { "events": [ { "record": { "id": "1" // ...other fields that match collection fieldsSchema } } ], "response": { "headers": { "x-response-header": "header value" }, "data": { "responseField": "value" } } } ``` Example: ```javascript theme={null} // Example: Handle a webhook and return events. export default async function handle({ body, query, parameters, rawBody }) { // Verify webhook signature if needed if (query.signatureRequest) { const signature = createHmac('sha256', parameters.webhookSecret).update(rawBody).digest('hex') if (signature !== headers['x-webhook-signature']) { throw new Error('Invalid webhook signature') } } // Process the webhook payload return { events: [ { type: 'created', // Possible values: 'created', 'updated', 'deleted' record: body.record, }, ], response: { headers: { 'Content-Type': 'text/plain', }, data: query?.data, }, } } ``` ### Unsubscribe * **Method key:** `unsubscribe` * **Supported implementation types:** `javascript`, `rest-api-mapping`, `graphql-api-mapping` This function should send a request to delete the webhook by `webhookId` received in the `subscribe` method. **Arguments:** * `state` – state returned from `subscribe` * `parameters` – object, parameters (matching `parametersSchema`) of this particular event Example: ```javascript theme={null} // Example: Unsubscribe from a webhook. export default async function unsubscribe({ apiClient, state, parameters }) { await apiClient.delete(`${parameters.path}/${state.webhookId}`) } ``` ### Refresh (optional) * **Method key:** `refresh` * **Supported implementation types:** `javascript`, `rest-api-mapping`, `graphql-api-mapping` Some applications require refreshing the subscription to keep it active.\ This method should be implemented if the `nextRefreshTimestamp` is returned from the `subscribe` method. **Arguments:** * `state` – state returned from `subscribe` * `webhookUri` – string, URI webhooks should be sent to * `parameters` – object, parameters (matching `parametersSchema`) of this particular event **Response:** * `state` – updated state information if it needs to be updated * `nextRefreshTimestamp` – number, timestamp in milliseconds when the next refresh should be performed Example: ```javascript theme={null} // Example: Refresh a webhook subscription. export default async function refresh({ apiClient, state, parameters }) { await apiClient.post(`${parameters.path}/${state.webhookId}/refresh`) return { nextRefreshTimestamp: Date.now() + 1000 * 60 * 60 * 24, // Refresh in 24 hours } } ``` # Field Mappings Source: https://docs.getmembrane.com/reference/workspace-elements/field-mappings Field Mappings allow you to transform data between your app and external apps when reading from or writing to a [Data Source](/reference/workspace-elements/data-sources). ## Configuring Field Mapping To configure field mapping, you provide: * External Fields define the [Data Schema](/docs/references/data-schemas) of the fields in the external app. * Internal Fields define the schema of fields in your app. It can be generated automatically based on the mapping configuration, defined statically, or calculated dynamically via [Internal Data Schemas](/reference/workspace-elements/app-data-schemas). * Direction: import, export, or bi-directional. * Export Value: mapping from your app to external app. * Import Value: mapping from external app to your app. Depending on the level of abstraction (universal, integration, or tenant level), different set of fields will be available for mapping: * Universal: fields from the Universal Data Model configured in the data source will be available. * Integration: standard fields from the Data Collection data source is pointing to will be available. * Tenant: connection-specific fields from the data collection that data source is pointing to will be available, including any custom fields. ## Applying Field Mapping When field mapping is applied in a Flow or an Action, the data will be transformed based on the tenant-level field mapping configuration. You will see the mapping and the result in the Flow or Action logs. ### Fields in Create and Update methods If `create` or `update` methods for a Data Collection mapping is used for specify supported fields, only those fields will stay in the mapping, ignoring values of the rest of the fields. This is done to let you use the same mapping for different methods, and to avoid errors due to extra fields. For example, if `type` field is supported in the `create` method, but not supported in the `update` method and you have this field in the field mapping, it will not be sent to the `update` method. ## Learn More Drop-in UI for a single field mapping. Representing data structures used in integrations. Configuring access to data in external apps. Reusing data schemas between integration elements. Reacting to events in your app. # Field Mapping Instances Source: https://docs.getmembrane.com/reference/workspace-elements/field-mappings/field-mapping-instances A field mapping instance is automatically created once a [Connection](/docs/how-membrane-works/connections) is established with an external app. By default, a Field Mapping instance carries the default mapping that was specified on the Field Mapping. Like the Field Mapping itself, the instance may have a two-way mapping: * `importValue` – mapping from external app schema to your app schema * `exportValue` – mapping from your app schema to external app schema A tenant can configure both the import and export sides of the field mapping instance. ## Why configure field mapping instance You'd want to have your user configure their field mapping instance if you want to have them control how properties in the external application map to field used by your app. ## Configuring Instance via Membrane UI The simplest way to configure a Field Mapping is to use the configuration UI: ```javascript theme={null} await membrane.connection('hubspot').fieldMapping('{FIELD_MAPPING_KEY}').openConfiguration() ``` This will open up a the field mapping instance and let the user configure both sides of the field mapping
See more: [Field Mapping UI](/reference/workspace-elements/field-mappings/field-mapping-instances) ## Creating & Configuring Instance via Membrane SDK Here is how to use Field Mapping Instances via Membrane SDK ### Create ```javascript theme={null} await membrane.connection('hubspot').fieldMapping('{FIELD_MAPPING_KEY}').create() ```
### Get ```javascript theme={null} await membrane.connection('hubspot').fieldMapping('{FIELD_MAPPINGS_KEY}').get() ```
### Update ```javascript theme={null} await membrane.connection('hubspot').fieldMapping('{FIELD_MAPPING_KEY}').patch('{INPUT}') ```
### Set up / Refresh Updates to the base field mapping are automatically applied. If they aren't, you can use `.setup()` to manually pull merged updates from the base field mapping. ```javascript theme={null} await membrane.connection('hubspot').fieldMapping('{FIELD_MAPPING_KEY}').setup() ```
### Reset This method resets the field mapping instance to its default state, erasing all the tenant configuration. ```javascript theme={null} await membrane.connection('hubspot').fieldMapping('{FIELD_MAPPING_KEY}').reset() ```
### Archive ```javascript theme={null} await membrane.connection('hubspot').fieldMapping('{FIELD_MAPPING_KEY}').archive() ```
## Multiple Field Mapping Instances per Connection By default, every pair of Field Mapping and Connection will use the same Field Mapping Instance, which means that mapping values will be shared between them. If you want to use multiple different mapping values for the same pair of Field Mapping + Connection, you should use `instanceKey`. ```javascript theme={null} await membrane .connection('hubspot') .fieldMapping('{FIELD_MAPPINGS_KEY}', { instanceKey: '{INSTANCE_KEY}' }) .get({ autoCreate: true }) ```
When you apply `instanceKey` to a Field Mapping Instance, it will be automatically applied to the elements Field Mapping works with: Data Source, App Data Collection, etc. # Files Source: https://docs.getmembrane.com/reference/workspace-elements/files # Flow Runs Source: https://docs.getmembrane.com/reference/workspace-elements/flow-runs # Flows Source: https://docs.getmembrane.com/reference/workspace-elements/flows Flow represents a multi-step integration logic that can be triggered by events, on schedule, or via API. Flows consist of nodes and always start with a trigger node. Here is an example of a flow: ```yaml theme={null} name: Sync New Contacts key: sync-new-contacts nodes: contact-created: type: data-record-created-trigger name: Contact Created config: dataSource: collectionKey: contacts links: - key: transform-contact-data transform-contact-data: type: transform-data name: Transform Contact Data config: output: firstName: $var: input.contact-created.record.fields.firstName lastName: $var: input.contact-created.record.fields.lastName email: $var: input.contact-created.record.fields.email links: - key: send-contact-to-my-app send-contact-to-my-app: type: api-request-to-your-app name: Send contact to my app config: request: uri: https://myapp.com/api/new-contact method: POST body: contact: $var: $.input.transform-contact-data ```
## Flow Structure A flow consists of the following components: * **`name`** – Human-readable name of the flow. * **`key`** – Name of the flow to use in your code (unique within a workspace) * **`description`** – Optional description of what the flow does. * **`nodes`** – A dictionary of nodes that make up the flow. Each node has a unique key and contains configuration for its behavior. * **`parametersSchema`** – Schema of the flow parameters - useful for creating child flows from a parent template. * **`parameters`** – Parameters of the flow (match `parametersSchema`) * **`enabled`** – Whether the flow is enabled. * **`parentUuid`** – UUID of the parent flow (when this flow was created from a parent flow) * **`integrationUuid`** – UUID of the integration this flow is created for (only for flows that have an integration) * **`connectionUuid`** – UUID of the connection this flow is created for (only for flows that have a connection) * **`isCustomized`** – Whether the flow is customized compared to the parent (only for flows that have a parent) ### Node Structure Each node in a flow has the following properties: * **`type`** – Node type that defines what the node does (see [Node Types](#node-types) below). * **`name`** – Human-readable name describing what this node does. * **`config`** – Configuration specific to the node type. * **`concurrency`** – Number of concurrent runs allowed for this node (default: 1). * **`onError`** – Behavior when the node encounters an error: `stop` (default) or `continue`. * **`links`** – Array of links to downstream nodes. ### Links Links connect nodes together and control the flow of data. Each link has: * **`name`** – Optional name for the link. * **`key`** – Key of the target node to connect to. * **`filter`** – Optional filter to conditionally pass outputs to the target node. The value of the filter can use any formulas and has access to the same variables as the node's configuration, with additional `output` variable available that represents output of the current node.
## Node Types Nodes are the building blocks of flows. Each node type serves a specific purpose and can be configured to perform different operations. ### Triggers Triggers start the flow execution. Every flow must have at least one trigger node. Flows can have multiple triggers to reuse the same logic in different scenarios. | Type | Description | Documentation | | :- | :- | :- | | `api-trigger` | Launch flow via API with optional input | [API Trigger](/reference/workspace-elements/flows/trigger-nodes#api-trigger) | | `schedule-trigger` | Run flow on a recurring schedule | [Schedule Trigger](/reference/workspace-elements/flows/trigger-nodes#schedule-trigger) | | `app-event-trigger` | Launch flow when an event happens in your app | [Internal Event Trigger](/reference/workspace-elements/flows/trigger-nodes#internal-event-trigger) | | `data-record-created-trigger` | Run flow when a data record is created | [Data Record Created Trigger](/reference/workspace-elements/flows/trigger-nodes#data-record-created-trigger) | | `data-record-updated-trigger` | Run flow when a data record is updated | [Data Record Updated Trigger](/reference/workspace-elements/flows/trigger-nodes#data-record-updated-trigger) | | `data-record-deleted-trigger` | Run flow when a data record is deleted | [Data Record Deleted Trigger](/reference/workspace-elements/flows/trigger-nodes#data-record-deleted-trigger) | | `connector-event-trigger` | Launch flow on connector-specific events | [Connector Event Trigger](/reference/workspace-elements/flows/trigger-nodes#connector-event-trigger) | ### Function Nodes Function nodes perform operations on data, make API requests, or interact with external systems. | Type | Description | Documentation | | :- | :- | :- | | `list-data-records` | List all records from a data collection | [List Data Records](/reference/workspace-elements/flows/function-nodes#list-data-records) | | `find-data-record-by-id` | Find a single record by its ID | [Find Data Record By ID](/reference/workspace-elements/flows/function-nodes#find-data-record-by-id) | | `search-data-records` | Search for records using a query string | [Search Data Records](/reference/workspace-elements/flows/function-nodes#search-data-records) | | `lookup-data-record` | Look up a record by field values | [Lookup Data Record](/reference/workspace-elements/flows/function-nodes#lookup-data-record) | | `find-or-create-data-record` | Find existing record or create if not found | [Find or Create Data Record](/reference/workspace-elements/flows/function-nodes#find-or-create-data-record) | | `create-data-record` | Create a new data record | [Create Data Record](/reference/workspace-elements/flows/function-nodes#create-data-record) | | `update-data-record` | Update an existing data record | [Update Data Record](/reference/workspace-elements/flows/function-nodes#update-data-record) | | `delete-data-record` | Delete a data record | [Delete Data Record](/reference/workspace-elements/flows/function-nodes#delete-data-record) | | `create-data-link` | Create a link between records | [Create Data Link](/reference/workspace-elements/flows/function-nodes#create-data-link) | | `find-data-link` | Find a link between records | [Find Data Link](/reference/workspace-elements/flows/function-nodes#find-data-link) | | `delete-data-link` | Delete a link between records | [Delete Data Link](/reference/workspace-elements/flows/function-nodes#delete-data-link) | | `api-request-to-external-app` | Make authenticated API requests to external apps | [API Request to External App](/reference/workspace-elements/flows/function-nodes#api-request-to-external-app) | | `api-request-to-your-app` | Make API requests to your internal API | [API Request to Your App](/reference/workspace-elements/flows/function-nodes#api-request-to-your-app) | | `custom-http-request` | Make arbitrary HTTP requests | [Custom HTTP Request](/reference/workspace-elements/flows/function-nodes#custom-http-request) | | `run-javascript` | Execute custom JavaScript code | [Run JavaScript](/reference/workspace-elements/flows/function-nodes#run-javascript) | | `run-action` | Execute a predefined action | [Run Action](/reference/workspace-elements/flows/function-nodes#run-action) | | `integration-specific-operation` | Execute integration-specific operations | [Integration Specific Operation](/reference/workspace-elements/flows/function-nodes#integration-specific-operation) | ### Control Nodes Control nodes manage the flow of execution, transform data, and implement conditional logic. | Type | Description | Documentation | | :- | :- | :- | | `transform-data` | Transform and reshape data | [Transform Data](/reference/workspace-elements/flows/control-nodes#transform-data) | | `filter` | Filter which records continue through the flow | [Filter](/reference/workspace-elements/flows/control-nodes#filter) | | `for-each-v2` | Execute nodes for each item in a list | [For Each](/reference/workspace-elements/flows/control-nodes#for-each) |
## Inputs and Outputs When a flow starts, the trigger node receives an initial input. This could be: * Data from an API request (API Trigger) * Data from an external app event (Data Record Created Trigger) * Empty object for scheduled triggers (Schedule Trigger) * Data from an internal app event (Internal Event Trigger) Each node produces an output after executing. This output is added to the input of all downstream nodes under the node's key. For example, if a node with key `transform` produces output `{firstName: "John", lastName: "Doe"}`, the downstream nodes will receive: ```json theme={null} { "trigger-key": { /* trigger output */ }, "transform": { "firstName": "John", "lastName": "Doe" } } ``` These outputs are available for subsequent nodes as `input` variable. ## Variables You can use variables in the node configuration. Here are the available variables: * `input` - Initial flow input + outputs of all upstream nodes * `flowInstance` - data of the current flow. * `flowRun` - data of the current flow run. * `integration` - data of the current integration * `connection` - data of the current connection * `user` - data of the current user * `parameters` - parameters of the current flow You can reference any upstream node's output using the `$var` syntax: ```yaml theme={null} config: output: currentIntegrationId: $var: integration.id previousNodeOutput: $var: input.previous-node ``` ## Using Flows You can only run a flow created for a specific connection (having connectionId field). It can be either flow created for a specific connection only or an integration-level or universal flow instantiated for a specific connection. If your flow has an API trigger, you can run it via API or SDK. Otherwise, a flow will be executed automatically when the trigger conditions are met. See full API reference here: [Flows API Reference](https://.integration.app/update/reference/workspace-elements/flows). Each time a flow runs, it creates a [Flow Run](/docs/managing-membrane/monitoring-troubleshooting/logs/flow-runs) record. ## API Operations | Operation | Method | Endpoint | Description | | - | - | - | - | | [List all flows](/api-reference/flows/list-flows) | GET | `/flows` | Retrieve all workspace flows | | [Get flow](/api-reference/flows/get-flow-by-selector) | GET | `/flows/{selector}` | Get specific flow details | | [Create a new flow](/api-reference/flows/create-a-new-flow) | POST | `/flows` | Create a new workspace flow | | [Replace flow](/api-reference/flows/replace-flow-completely) | PUT | `/flows/{selector}` | Replace flow configuration | | [Patch flow](/api-reference/flows/update-flow-partially) | PATCH | `/flows/{selector}` | Update flow partially | | [Clone flow](/api-reference/flows/clone-flow) | POST | `/flows/{selector}/clone` | Clone an existing flow | | [Reset flow](/api-reference/flows/reset-flow) | POST | `/flows/{selector}/reset` | Reset flow to default state | | [Export flow](/api-reference/flows/export-flow-flat-interface) | GET | `/flows/{selector}/export` | Export flow configuration | | [Get flow dependencies](/api-reference/flows/get-dependencies-of-flow-flat-interface) | GET | `/flows/{selector}/dependencies` | Get flow dependencies | | [Restore flow](/api-reference/flows/restore-flow-flat-interface) | POST | `/flows/{selector}/restore` | Restore archived flow | | [Refresh flows](/api-reference/flows/refresh-flow) | POST | `/flows/refresh` | Refresh flow definitions | | [Archive flow](/api-reference/flows/archive-flow) | DELETE | `/flows/{selector}` | Archive a flow | # Control Nodes Source: https://docs.getmembrane.com/reference/workspace-elements/flows/control-nodes Control nodes manage the flow of execution, transform data, and implement conditional logic. They help you build complex workflows with branching, filtering, and iteration.
## Transform Data **Type:** `transform-data` The Transform Data node transforms input into a new data structure. This is one of the most commonly used nodes, essential for reshaping data between different formats. ```yaml theme={null} transform: type: transform-data name: Format Contact Data config: output: fullName: $eval: $concat: - $var: input.trigger.fields.firstName - ' ' - $var: input.trigger.fields.lastName email: $var: input.trigger.fields.email createdAt: $eval: $now: {} metadata: source: external-app syncedAt: $eval: $now: {} ``` The `output` configuration defines the structure of the transformed data. You can use variables (`$var`), expressions (`$eval`), or static values. **Output:** Returns the transformed object as specified in the `output` configuration: ```javascript theme={null} { fullName: 'John Doe', email: 'john@example.com', createdAt: '2024-01-15T10:30:00Z', metadata: { source: 'external-app', syncedAt: '2024-01-15T10:30:00Z' } } ```
## Filter **Type:** `filter` The Filter node conditionally allows data to continue through the flow. Only inputs that match the filter criteria are passed to downstream nodes. ```yaml theme={null} filter: type: filter name: Only Active Contacts config: filter: $eval: $var: input.findContact.fields.status $eq: active ``` **Configuration options:** * **`filter`** – Filter expression using evaluation syntax (supports `$eq`, `$gt`, `$and`, `$or`, etc.) **Output:** The filter node passes through the input unchanged if it matches the filter. If the input doesn't match, no output is produced and the flow stops for that input.
## For Each **Type:** `for-each-v2` The For Each node executes a subset of flow nodes for each item in a list, then aggregates their outputs. This is essential for processing arrays of data. ```yaml theme={null} forEach: type: for-each-v2 name: Process Each Contact config: items: $var: input.listContacts rootNodeKey: processContact links: - key: summary processContact: type: transform-data name: Transform Contact config: output: id: $var: input.forEach.item.id fullName: $eval: $concat: - $var: input.forEach.item.fields.firstName - ' ' - $var: input.forEach.item.fields.lastName links: - key: createInCRM createInCRM: type: create-data-record name: Create in CRM config: dataSource: collectionKey: contacts fieldMapping: defaultValue: name: $var: input.processContact.fullName summary: type: transform-data name: Create Summary config: output: processed: $var: input.forEach allCreatedContacts: $var: input.forEach.createInCRM ``` **Configuration options:** * **`items`** – Array of items to iterate over (usually from a previous node's output) * **`rootNodeKey`** – Key of the first node inside the For Each loop **How it works:** 1. The For Each node receives a list of items 2. For each item, it executes the nodes inside the loop (starting from `rootNodeKey`) 3. Inside the loop, the current item is available as `input.forEach.item` (where `forEach` is the key of your for-each node) 4. All nodes inside the loop run for each item 5. After all items are processed, outputs are aggregated into a list **Accessing the current item:** Inside the For Each loop, access the current item: ```yaml theme={null} $var: input.forEach.item $var: input.forEach.item.id $var: input.forEach.item.fields.email ``` **Output structure:** The For Each node outputs an object with keys matching the nodes inside the loop, and values being arrays of their outputs: ```javascript theme={null} { processContact: [ { id: '1', fullName: 'John Doe' }, { id: '2', fullName: 'Jane Smith' } ], createInCRM: [ { id: 'crm-1', name: 'John Doe' }, { id: 'crm-2', name: 'Jane Smith' } ] } ``` # Flow UI Source: https://docs.getmembrane.com/reference/workspace-elements/flows/flow-ui To let a user configure a Flow Instance, call the `openConfiguration` method: ```javascript theme={null} await membrane.connection('hubspot').flow('{FLOW_KEY}').openConfiguration() ```
### Flow Instance editor To edit a Flow Instance, call the `openEditor` method: ```javascript theme={null} await membrane.flowInstance('{FLOW_INSTANCE_ID}').openEditor() ```
Alternatively, you can embed the Flow Instance editor directly into a specific element: ```javascript theme={null} function FlowBuilderEmbedding() { const targetId = 'flow-builder-emdedding' const membrane = useIntegrationApp() const targetRef = useRef < HTMLDivElement > null useEffect(() => { void membrane .flowInstance({ flowKey: parameters.FLOW_KEY, integrationKey: parameters.INTEGRATION_KEY, autoCreate: true, }) .embedEditor({ mountTargetSelector: `[data-container-id="${targetId}"]`, }) }, [parameters.FLOW_KEY, parameters.INTEGRATION_KEY, membrane]) return
} ```
## See Also Managing tenant flows instances from inside your applications. # Configure Flow Instance Parameters Source: https://docs.getmembrane.com/reference/workspace-elements/flows/flow-ui/configure-flow-instance-parameters # Configure Flow Instance Parameters To set flow parameters, patch the `parameters` property of the Flow Instance: ```javascript theme={null} await membrane.flowInstance(parameters.FLOW_INSTANCE_ID).patch({ enabled: true }) ```
The parameters must match the Parameters Schema you configured for the flow. # Custom Flow UI Source: https://docs.getmembrane.com/reference/workspace-elements/flows/flow-ui/custom-flow-ui To build custom white-label UI for a flow, use one of the following articles: Let your users decide whether ornot to run this integration scenario. Let your users configure the integration logic for a given flow instance. To build custom UI for components of the flow, follow the following articles: Customizing data sources from inside your app. Letting your users map fields between your app and external apps. # Enable and Disable Flow Instances Source: https://docs.getmembrane.com/reference/workspace-elements/flows/flow-ui/enable-and-disable-flow-instances-to To enable or disable a specific [Flow Instance](/reference/workspace-elements/flows), patch its `enabled` property: ```javascript theme={null} await membrane.flowInstance(parameters.FLOW_INSTANCE_ID).patch({ enabled: true }) ``` Flow Instances are enabled by default but you can create a disabled flow instances using the `enabled` field e.g ``` await membrane .flowInstances.create({ enabled: false }) ``` # Function Nodes Source: https://docs.getmembrane.com/reference/workspace-elements/flows/function-nodes Function nodes perform operations on data, make API requests, or interact with external systems. They are the core building blocks for implementing integration logic in flows. Many function nodes use [Function Types](/docs/references/functions) to implement their logic. The configuration and behavior is defined by the corresponding function type.
## Data Record Operations These nodes operate on data records in external applications. Most of them map directly to function types. | Node Type | Description | Function Type | | :- | :- | :- | | `list-data-records` | List all records from a data collection | [List Data Records](/docs/references/functions/function-types/list-data-records) | | `find-data-record-by-id` | Find a single record by its ID | [Find Data Record By ID](/docs/references/functions/function-types/find-data-record-by-id) | | `search-data-records` | Search for records using a query string | [Search Data Record](/docs/references/functions/function-types/search-data-record) | | `lookup-data-record` | Look up a record by field values | [Match Data Record](/docs/references/functions/function-types/match-data-record) | | `create-data-record` | Create a new data record | [Create Data Record](/docs/references/functions/function-types/create-data-record) | | `update-data-record` | Update an existing data record | [Update Data Record](/docs/references/functions/function-types/update-data-record) | | `delete-data-record` | Delete a data record | [Delete Data Record](/docs/references/functions/function-types/delete-data-record) |
## Find or Create Data Record **Type:** `find-or-create-data-record` Attempts to find a record by specified criteria. If no record is found, creates a new one. This is useful for upserting data. ```yaml theme={null} findOrCreate: type: find-or-create-data-record name: Find or Create Contact config: dataSource: collectionKey: contacts lookupFields: email: $var: input.trigger.email createData: firstName: $var: input.trigger.firstName lastName: $var: input.trigger.lastName email: $var: input.trigger.email fieldMapping: key: contact-mapping ``` **Configuration options:** * **`dataSource`** – Specifies which [Data Source](/reference/workspace-elements/data-sources) to use * **`lookupFields`** – Fields to match when searching for existing records * **`createData`** – Data to use when creating a new record * **`fieldMapping`** – [Field Mapping](/reference/workspace-elements/field-mappings) for the operation **Output:** Returns the found or newly created data record: ```javascript theme={null} { id: 'contact-456', name: 'Jane Smith', fields: { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }, created: false // true if record was created, false if found } ```
## Create Data Link **Type:** `create-data-link` Creates a [Data Link](/reference/workspace-elements/data-links) between an object in your app and an object in an external app. ```yaml theme={null} createLink: type: create-data-link name: Link Contact config: dataLinkTable: contact-links internalId: $var: input.trigger.internalContactId externalId: $var: input.createRecord.id direction: both ``` **Configuration options:** * **`dataLinkTable`** – The Data Link table to use (must be created first) * **`internalId`** – ID of the object in your app * **`externalId`** – ID of the object in the external app * **`direction`** – Link direction: `export` (internal → external), `import` (external → internal), or `both` If a link already exists, it will be updated. **Output:** Returns the created or updated data link: ```javascript theme={null} { id: 'link-789', internalId: 'internal-contact-123', externalId: 'external-contact-456', direction: 'both' } ```
## Find Data Link **Type:** `find-data-link` Finds a [Data Link](/reference/workspace-elements/data-links) to get the corresponding object ID on the opposite side. ```yaml theme={null} findLink: type: find-data-link name: Find Linked Contact config: dataLinkTable: contact-links internalId: $var: input.trigger.internalContactId ``` **Configuration options:** * **`dataLinkTable`** – The Data Link table to search * **`internalId`** – ID to look up (can be internal or external depending on direction) **Output:** Returns the ID of the matching record on the opposite side, or null if no link is found: ```javascript theme={null} { externalId: 'external-contact-456' } // or null if not found ```
## Delete Data Link **Type:** `delete-data-link` Deletes an existing [Data Link](/reference/workspace-elements/data-links). ```yaml theme={null} deleteLink: type: delete-data-link name: Unlink Contact config: dataLinkTable: contact-links internalId: $var: input.trigger.internalContactId ``` **Configuration options:** * **`dataLinkTable`** – The Data Link table containing the link * **`internalId`** – ID of the linked object **Output:** Returns confirmation of deletion: ```javascript theme={null} { success: true, deletedLinkId: 'link-789' } ```
## HTTP Request Operations These nodes make HTTP requests to APIs and external services. | Node Type | Description | Function Type | | :- | :- | :- | | `api-request-to-external-app` | Make authenticated API requests to external apps | [API Request to External App](/docs/references/functions/function-types/api-request-to-external-app) | | `api-request-to-your-app` | Make API requests to your internal API | [API Request to Your App](/docs/references/functions/function-types/api-request-to-your-app) | | `custom-http-request` | Make arbitrary HTTP requests to any endpoint | [HTTP Request](/docs/references/functions/function-types/http-request) |
## Code Execution These nodes execute code and run predefined actions. | Node Type | Description | Function Type | | :- | :- | :- | | `run-javascript` | Execute custom JavaScript or TypeScript code | [JavaScript](/docs/references/functions/function-types/javascript) | | `run-action` | Execute a predefined action | [Run Action](/docs/references/functions/function-types/run-action) |
## Integration Specific Operation **Type:** `integration-specific-operation` Executes operations that are specific to a particular integration. These are pre-built operations provided by connectors. ```yaml theme={null} specialOp: type: integration-specific-operation name: Send Email config: operationKey: send-email parameters: to: $var: input.findContact.fields.email subject: Welcome! body: $var: input.transform.emailBody ``` **Configuration options:** * **`operationKey`** – Key of the integration-specific operation * **`parameters`** – Parameters required by the operation The available operations and their parameters depend on the specific connector being used. Refer to connector documentation for details. **Output:** Returns the result of the operation. Structure varies by operation: ```javascript theme={null} { success: true, messageId: 'msg-123', // ... other operation-specific fields } ``` # Trigger Nodes Source: https://docs.getmembrane.com/reference/workspace-elements/flows/trigger-nodes Trigger nodes are special nodes that start flow execution. Every flow must have at least one trigger. A flow can have multiple triggers if you want to reuse the same integration logic in different scenarios.
## API Trigger **Type:** `api-trigger` The API Trigger allows you to launch a Flow Instance with optional input using your application's API or SDK. ```yaml theme={null} trigger: type: api-trigger name: Manual Sync Trigger config: inputSchema: type: object properties: recordId: type: string syncAll: type: boolean ``` **Configuration options:** * **`inputSchema`** – Optional schema that will be validated when the flow is triggered **Output:** The output contains the input provided when triggering the flow: ```javascript theme={null} { recordId: '12345', syncAll: false } ```
## Schedule Trigger **Type:** `schedule-trigger` The Schedule Trigger runs a flow at regular intervals as long as the Flow Instance is enabled. ```yaml theme={null} trigger: type: schedule-trigger name: Hourly Sync config: intervalMinutes: 60 ``` **Configuration options:** * **`intervalMinutes`** – How often the flow should run (5 = every 5 minutes, 60 = hourly, 1440 = daily) **Output:** The output contains timing information: ```javascript theme={null} { triggeredAt: '2024-01-15T10:30:00Z' } ```
## Internal Event Trigger **Type:** `app-event-trigger` The Internal Event Trigger runs a flow when a specific event occurs in your application. Before using this trigger, you need to create an [Internal Event](/docs/managing-membrane/monitoring-troubleshooting/logs/internal-events). ```yaml theme={null} trigger: type: app-event-trigger name: Order Created Event config: appEventKey: order-created filter: $eval: $var: $.payload.amount $gt: 1000 ``` **Configuration options:** * **`appEventKey`** – Key of the internal event to listen to * **`filter`** – Optional filter to conditionally trigger the flow based on event data **Output:** The output contains the event payload: ```javascript theme={null} { payload: { orderId: '12345', amount: 1500, customer: 'customer-id' } } ```
## Data Record Created Trigger **Type:** `data-record-created-trigger` This trigger launches a flow when a new data record is created in a selected Data Source. ```yaml theme={null} trigger: type: data-record-created-trigger name: New Contact Created config: dataSource: collectionKey: contacts fieldMapping: key: contact-mapping defaultValue: firstName: $var: $.rawFields.first_name lastName: $var: $.rawFields.last_name email: $var: $.rawFields.email ``` **Configuration options:** * **`dataSource`** – Specifies which [Data Source](/reference/workspace-elements/data-sources) collection to monitor * **`fieldMapping`** – Optional [Field Mapping](/reference/workspace-elements/field-mappings) to transform data from the external app to your app's format **Output:** The output contains information about the created record: ```javascript theme={null} { id: 'contact-123', name: 'John Doe', url: 'https://app.com/contacts/123', fields: { firstName: 'John', lastName: 'Doe', email: 'john@example.com' }, rawFields: { first_name: 'John', last_name: 'Doe', email: 'john@example.com' } } ```
## Data Record Updated Trigger **Type:** `data-record-updated-trigger` This trigger launches a flow when an existing data record is updated in a selected Data Source. ```yaml theme={null} trigger: type: data-record-updated-trigger name: Contact Updated config: dataSource: collectionKey: contacts fieldMapping: key: contact-mapping ``` **Configuration options:** * **`dataSource`** – Specifies which [Data Source](/reference/workspace-elements/data-sources) collection to monitor * **`fieldMapping`** – Optional [Field Mapping](/reference/workspace-elements/field-mappings) to transform updated data **Output:** The output contains the updated record data in the same format as Data Record Created Trigger.
## Data Record Deleted Trigger **Type:** `data-record-deleted-trigger` This trigger launches a flow when a data record is deleted in a selected Data Source. ```yaml theme={null} trigger: type: data-record-deleted-trigger name: Contact Deleted config: dataSource: collectionKey: contacts fieldMapping: key: contact-mapping ``` **Configuration options:** * **`dataSource`** – Specifies which [Data Source](/reference/workspace-elements/data-sources) collection to monitor * **`fieldMapping`** – Optional [Field Mapping](/reference/workspace-elements/field-mappings) to transform data about the deleted record **Output:** The output contains information about the deleted record. Note that some external apps may only provide the record ID. ```javascript theme={null} { id: 'contact-123', name: 'John Doe' } ```
## Connector Event Trigger **Type:** `connector-event-trigger` The Connector Event Trigger launches flows in response to connector-specific events. ```yaml theme={null} trigger: type: connector-event-trigger name: Custom Webhook Event config: eventKey: custom-event filter: $eval: $var: $.payload.status $eq: completed ``` **Configuration options:** * **`eventKey`** – Key of the event to trigger on * **`filter`** – Optional filter to conditionally trigger the flow **Output:** The output structure depends on the specific connector and event type. Refer to the connector's documentation for details. # Incoming Webhooks Source: https://docs.getmembrane.com/reference/workspace-elements/incoming-webhooks # Integrations Source: https://docs.getmembrane.com/reference/workspace-elements/integrations Integration represents a relationship between your product and a given external app. ## Integration Definition Integration looks like this: ```yaml theme={null} name: Github key: github logoUri: https://static.integration.app/connectors/github/logo.png appUuid: c04f4af3-1704-4693-86a4-9af453205335 connectorId: 62471fad996d5c297ae43f5f connectorVersion: 2.3.1 authOptions: - title: API Key description: Connect using one of the Github API key types type: client-credentials - title: OAuth description: Connect in one click by approving this app's access type: oauth2 ``` In this definition: * `logoUri` - URL of the publicly available image with the integration logo. * `appUuid` - uuid of the [External App](/reference/workspace-elements/external-apps) this integration is related to. * `connectorId` - id of the [Connector](/reference/workspace-elements/connectors) this integration uses. * `connectorVersion` - a published version of the connector. If empty, development (unpublished) version of the connector is used. * `authOptions` (read-only) - list of available authentication options based on the connector and integration parameters. The format of auth options matches the auth config from the [Connector](/reference/workspace-elements/connectors) definition with all the [Formulas](/docs/references/formulas) resolved (if there were any). ## Connector Parameters If the connector used by an integration has [parameters](/reference/workspace-elements/connectors#connector-parameters) (for example `clientId` and `clientSecret` for OAuth), you can store their values on the integration level. For security reasons, parameters are not saved as a part of the integration definition and are not returned in the response by default. Read them by fetching the integration with `?includeSecrets=true` (the response includes both `parameters` and `optionsConfig`), and write them with [Patch integration](/api-reference/integrations/update-integration-partially). Where you put them depends on the connector's auth options: * **Single auth option** — store the values at the top level, under `parameters`: ```json theme={null} { "parameters": { "clientId": "...", "clientSecret": "..." } } ``` * **[Multiple auth options](/reference/workspace-elements/connectors#connector-options)** — store each option's values under `optionsConfig..parameters`: ```json theme={null} { "optionsConfig": { "oauth": { "parameters": { "clientId": "...", "clientSecret": "..." } } } } ``` `PATCH` replaces `parameters` and `optionsConfig` as **whole objects** — there's no deep merge. Send the full value you want to keep; any option you omit from `optionsConfig` is cleared. The [Get](/api-reference/integrations/get-integration-parameters) and [Set integration parameters](/api-reference/integrations/set-integration-parameters) endpoints are a convenience for the top-level `parameters` only and don't cover per-option values. If you're updating parameters programmatically, prefer the `?includeSecrets=true` + [Patch integration](/api-reference/integrations/update-integration-partially) approach above — it covers both. In the UI, you can edit these values in the integration config. For a connector with a single auth option, they appear in the **Parameters** section: Top-level connector parameters For a connector with multiple auth options, each option has its own parameters under **Options**: Per-auth-option connector parameters # Integrations UI Source: https://docs.getmembrane.com/reference/workspace-elements/integrations/integrations-catalog We recommend always building your own UI for displaying integrations (it is really simple!). That said, membrane also offers a few pre-built UI components for displaying your integrations. ## Building Integration UI * [Custom Integrations Catalog UI](/reference/workspace-elements/integrations/integrations-catalog/custom-list-of-integrations) * [Custom Integration UI](/reference/workspace-elements/integrations/integrations-catalog/custom-integration-ui) ## Integration Catalog UI This component displays a full list of integrations in your workspace. ```javascript theme={null} await membrane.open() ``` ## Single Integration UI To let your user connect or configure a single integration, do the following: ```javascript theme={null} await membrane.integration('integration-key').open() ``` # Custom Integration UI Source: https://docs.getmembrane.com/reference/workspace-elements/integrations/integrations-catalog/custom-integration-ui This article describes how to build UI for configuring a single integration after your tenant created a Connection. If you are looking for how to create a Connection in the first place, check out [Custom Connection UI](/docs/ways-to-use-membrane/embedded-ui/connection-ui/custom-connection-ui). ## Get Integration To get information about the integration and its connection, do the following: ```javascript theme={null} const integration = await membrane.integration('hubspot').get() ```
To understand the state of the connection for a given integration, look at the following fields to display in the UI: | Field | Description | | - | - | | `connection` | If this integration has a connection established for the current user, this field will be non-empty | | `connection.disconnected` | If a connection is disconnected and requires user to re-connect, this flag will be set to true. | ## Re-connect and Disconnect You can use the following code to re-connect an integration if it was disconnected or if the user wants to re-connect for any other reason: ```javascript theme={null} await membrane.integration('hubspot').openNewConnection() ```
Or a custom version of this UI: [Custom Connection UI](/docs/ways-to-use-membrane/embedded-ui/connection-ui/custom-connection-ui). You can disconnect the integration using the following code: ```javascript theme={null} await membrane.connection('hubspot').archive() ```
## Manage Tenant-level Elements To know which [Flows](/reference/workspace-elements/flows), [Actions](/docs/how-membrane-works/actions), [Data Sources](/reference/workspace-elements/data-sources), or [Field Mappings](/reference/workspace-elements/field-mappings) are configured for the current integration, you can get their list this way: ```javascript theme={null} const { items: flowInstances } = await membrane.connection('hubspot').flows.list() const { items: actionInstances } = await membrane.connection('hubspot').actions.list() const { items: dataSourceInstances } = await membrane.connection('hubspot').dataSources.list() const { items: fieldMappingInstances } = await membrane.connection('hubspot').fieldMappings.list() ```
Then you can use the corresponding elements API and UI to manage them: | Element | API | UI | | - | - | - | | Flow | [Flows API](/reference/workspace-elements/flows) | [Flows UI](/reference/workspace-elements/flows/flow-ui/custom-flow-ui) | | Data Source | [Data Sources API](/reference/workspace-elements/data-sources/data-source-instances) | [Data Sources UI](/reference/workspace-elements/data-sources/data-source-instances) | | Field Mapping | [Field Mappings API](/reference/workspace-elements/field-mappings/field-mapping-instances) | [Field Mappings UI](/reference/workspace-elements/field-mappings/field-mapping-instances) | # Custom Integrations Catalog UI Source: https://docs.getmembrane.com/reference/workspace-elements/integrations/integrations-catalog/custom-list-of-integrations # UI for Managing Connections *Display the list of integrations, connect and disconnect external apps, configure connections.* ## List Integrations Your integration experience likely starts with a list of integrations your user can enable. To get this list, use the following code: ```javascript theme={null} const { items: integrations } = await membrane.integrations.find() ``` Integrations that already have a Connection created will have a `connection` property.\ You can use it to indicate that the integration is already connected. You can manage the list of available integrations on the [Integrations](/reference/workspace-elements/connectors) page. ## List Connections If you want to list all the connections a user has created without listing all the available integrations, do this: ```javascript theme={null} const { items: connections } = await membrane.connections.find() ```
## Connect To connect a user's account in a specific third-party app, call this method: ```javascript theme={null} await membrane.integration('hubspot').openNewConnection() ```
Or if you want to create your own authentication UI, check out this guide: [Custom Connection UI](/docs/ways-to-use-membrane/embedded-ui/connection-ui/custom-connection-ui) ## Disconnect To disconnect a specific connection, call this method: ```javascript theme={null} await membrane.connection('hubspot').archive() ```
## Next Steps Building "Connect" UI that fully blends into your app Managing tenant flows instances from inside your application. Customizing data sources from inside your app. Letting your users map fields between your app and external apps. # List integrations Source: https://docs.getmembrane.com/reference/workspace-elements/integrations/list-integrations GET /integrations `GET /integrations` returns the workspace's integration catalog. Beyond the user's own integrations, the response can include **auto-generated** entries discovered from connectors, external apps, and the public web — what we call "infinite integrations". ## Sources | Source | When it's included | | - | - | | Workspace integrations | Always. | | Connectors | Workspace setting `autoGenerateIntegrationsFromConnectors`, or `connectors=1`, or `all=1`. | | External apps | Workspace setting `autoGenerateIntegrationsFromExternalApps`, or `externalApps=1`, or `all=1`. | | Web search | Workspace setting `autoGenerateIntegrationsFromWebSearch`, or `websearch=1`, or `all=1`. Requires a non-empty `search`. | `all=1` force-enables every source. Auto-generated items have `isReadOnly: true` and a synthetic `key` (e.g. `~connector.membrane`, `~app.membrane`, `~search.membrane--getmembrane_com`) that you can pass to any endpoint accepting an integration selector. ## Deduplication The same canonical app can surface from more than one source — a connector for Membrane, an external app at `https://getmembrane.com`, and a web-search hit for the same brand all referring to the same product. The endpoint collapses these into a single row. **Pages may be smaller than `limit`.** Dedup runs after pagination, so a request for `limit=20` can return fewer items when duplicates are dropped. The cursor is unaffected; keep paginating until the response has no `cursor`. ## Search Set the `search` query parameter to filter the catalog. When the web-search source is enabled, search dispatches a single web-search call per request and caches results for 24h at the org level. # Packages Source: https://docs.getmembrane.com/reference/workspace-elements/packages Integration Packages are collections of integration elements. Packages can be organized around external apps (i.e. "Integration with Github"), integration scenarios (i.e. "Unified API for Tasks"), product features (i.e. "Documents Import") or any other grouping. Example of a package: ```yaml theme={null} name: Continuously Import Users shortDescription: Import users from external app and keep them in sync by monitoring events. description: elements: - type: action id: - type: flow id: ``` Packages can be universal (work across multiple integrations) and integration-specific. When a universal package is applied to a given integration (by creating a child package for that integration that has `parentId` pointing to the universal one), elements of the integration-specific packages are automatically generated and the list of elements can only be changed in the parent package. ## API Operations | Operation | Method | Endpoint | Description | | - | - | - | - | | [List all packages](/api-reference/packages/list-packages) | GET | `/packages` | Retrieve all available packages | | [Get package](/api-reference/packages/get-package-by-selector) | GET | `/packages/{selector}` | Get specific package details | | [Create a new package](/api-reference/packages/create-a-new-package) | POST | `/packages` | Create a new package | | [Replace package](/api-reference/packages/replace-package-completely) | PUT | `/packages/{selector}` | Replace package configuration | | [Patch package](/api-reference/packages/update-package-partially) | PATCH | `/packages/{selector}` | Update package partially | | [Clone package](/api-reference/packages/clone-package) | POST | `/packages/{selector}/clone` | Clone an existing package | | [Reset package](/api-reference/packages/reset-package) | POST | `/packages/{selector}/reset` | Reset package to default | | [Export package](/api-reference/packages/export-package-flat-interface) | GET | `/packages/{selector}/export` | Export package configuration | | [Get package dependencies](/api-reference/packages/get-dependencies-of-package-flat-interface) | GET | `/packages/{selector}/dependencies` | Get package dependencies | | [Restore package](/api-reference/packages/restore-package-flat-interface) | POST | `/packages/{selector}/restore` | Restore archived package | | [Refresh package](/api-reference/packages/refresh-package) | POST | `/packages/{selector}/refresh` | Refresh package from source | | [Archive package](/api-reference/packages/archive-package) | DELETE | `/packages/{selector}` | Archive a package | # Search Source: https://docs.getmembrane.com/reference/workspace-elements/search # Agent Sessions Source: https://docs.getmembrane.com/reference/workspace-elements/sessions # Tenants Source: https://docs.getmembrane.com/reference/workspace-elements/tenants Tenants represent the people, teams, organizations, apps, or agents that use integrations in your workspace. Every tenant has their own set of connections, element customizations, and activity logs. Membrane creates tenants automatically when a new ID appears in an [Access Token](/docs/how-membrane-works/authentication). You can also create and manage them using the API endpoints below. For a conceptual overview of how tenants work, data visibility, and recommended usage patterns, see [Tenants](/docs/how-membrane-works/tenants) in the docs. ## Tenant Structure * `id` — Unique identifier of the tenant * `internalId` — Your system's identifier for this tenant (unique per workspace) * `name` — Display name shown in the Membrane Console * `fields` — Arbitrary metadata object for storing tenant-specific data (e.g., API keys, preferences, plan tier) * `credentials` (secret) — Encrypted credentials associated with the tenant * `createdAt` — Timestamp when the tenant was created * `aiCreditsRolling30DayLimit` — Per-tenant AI credit limit for a rolling 30-day window (null for unlimited) ## Tenant Fields Each tenant has `fields` that contain any information you need to use in your integration logic. For example, you can store a user's API Token in `fields.apiToken` and use it to make requests to your API from within integrations. ### Defining the Field Schema You can define the schema for tenant fields in the **"Tenant Field Schemas"** section of the [Settings > General](https://console.integration.app/w/0/settings/general) page. **Important:** Tenant fields will not appear in the Console UI unless you define their schema first. While you can still set field values via authentication tokens or the API, they will only become visible in the Console after you: 1. Navigate to Settings > General 2. Define the field schema in the "Tenant Field Schemas" section 3. Specify the field names, types, and other properties ### Setting Field Values You can set tenant fields in two ways: 1. By adding a `fields` claim to the [Authentication Token](/docs/how-membrane-works/authentication). Whenever Membrane sees a new set of fields in the token, it saves them to be used going forward. 2. By updating tenants using the PATCH or PUT endpoints below. ## Connections Each tenant can have multiple connections to external applications. See [Connections](/docs/how-membrane-works/connections) for details. ## Tenant-level Customizations Tenants can have their own customizations of workspace-level elements (actions, flows, data sources, field mappings). This allows tenant-specific configuration while inheriting the workspace-level template. To create a tenant-level customization: * Go to the [Tenants](https://console.integration.app/w/0/tenants) page * Select the tenant * Navigate to the element type (e.g., Flows) inside the tenant's page * Create or customize an element