GET /api/config-surface: the machine-readable config catalog
Every page under reference/config-v3/ and reference/event-flow-manifests/
describes the config syntax by hand, for a human reading the docs. GET /api/config-surface describes the same syntax as data: the closed set of
step actions, transforms, when conditions and event source types your org
can use, plus the full JSON Schema of the config document itself. It is the
same catalog the in-app editor’s live validation and error messages are
generated from, so it never drifts from what a PATCH /draft will actually
accept.
Use it to:
- Discover the full action/transform/source vocabulary without reading step-actions.md top to bottom.
- Drive tooling: a linter, an IDE plugin, a codegen script for your own config authoring pipeline.
- Validate a connector config document structurally before you push it (see below).
Calling it
It is a plain authenticated GET, same Bearer contract as every other
endpoint in the config plane. An org API key works well here since this is
typically called from a script rather than an interactive session:
curl -s https://app.plugsync.com/api/config-surface \
-H "Authorization: Bearer $ORG_API_KEY" | jq 'keys_unsorted'
# -> ["actions", "transforms", "when", "sources", "json_schema", "plugin_embedded_fields"]No path parameters, no query parameters, no request body. The response is the same for every org: it describes what the platform supports, not anything org-specific.
Response structure
actions
One entry per step action (upsert, find, transform, plugin, …):
{
"name": "transform",
"description": "Project the field mappings of one schema into another (source schema -> canonical, or canonical -> step output).",
"category": "mapping",
"min_version": "3.0",
"fields": [
{"name": "entity", "type": "ref_entity", "required": true, "description": "", "default": null, "enum_values": null},
{"name": "to_schema", "type": "ref_schema", "required": true, "description": "Destination schema; 'canonical' writes $.canonical.", "default": null, "enum_values": null},
{"name": "from_schema", "type": "ref_schema", "required": true, "description": "Source schema whose field_mappings are applied.", "default": null, "enum_values": null}
],
"required_checks": [
{"fields": ["entity", "to_schema", "from_schema"], "message": "transform step requires entity, to_schema and from_schema", "mode": "any_falsy", "equals": null}
],
"limits": [],
"examples": []
}min_versionis"3.0"or"3.1": the 5 bulk actions (upsert_many,create_many,update_many,delete_many,associate_many) requireversion: "3.1"on the connector config.required_checksnames the fields the runtime rejects a step for omitting, and the exact error message it raises. These are enforced at draft-save/publish time, not by the JSON Schema below.limitsdocuments a hard architectural constraint on that action (a wall, not a bug): text meant to be shown verbatim in a UI or error message.examples, when present, are complete valid steps for shapes that are not obvious from the field table alone.
This is the same data step-actions.md narrates by hand, action by action, with worked examples and edge cases the catalog itself does not carry.
transforms
One entry per transform usable inside a field_mappings entry’s
transform list:
{"name": "lowercase", "kind": "builtin", "description": "Convert string to lowercase. Non-strings pass through.", "args": []}{
"name": "split", "kind": "parametric",
"description": "Split a string on a given separator and take an index. Lenient.",
"args": [
{"name": "sep", "type": "string", "required": true, "description": "Separator to split on (NOT whitespace)."},
{"name": "index", "type": "any", "required": false, "description": "Index of the part to keep (default 0)."},
{"name": "default", "type": "any", "required": false, "description": "Value returned when the index is out of range (instead of the unchanged input)."}
]
}kind: "builtin" takes no arguments; kind: "parametric" declares its
args the same way an action declares fields.
when
One entry per when condition variant (exists, found, equals, gt,
lt, in, not, all):
{"key": "exists", "value_type": "jsonpath", "description": "True if the $-ref resolves to a non-null value."}value_type says what shape the condition’s value takes: a JSONPath
string, an arbitrary object, a nested condition, or a list of nested
conditions. See step-actions.md for
the full semantics of each.
sources
One entry per event source type (event_api, webhook, poller,
scheduled, hubspot, rest, file):
{
"type": "poller",
"push": false,
"description": "REST pull: GET on a cron schedule, one event per item; optional credential auth, static and secret headers, and page-following.",
"fields": [
{"name": "inbound.url", "type": "string", "required": true, "description": "GET endpoint. ..."},
{"name": "inbound.schedule", "type": "cron", "required": false, "description": "Cron expression; empty fires every worker tick."}
],
"limits": [
{"id": "poller_no_hmac_signing", "text": "Auth supports bearer/basic/api-key headers and OAuth2 client credentials via a bound credential, plus static and secret headers; creds-in-body login and HMAC-signed batch polling are not implemented."}
]
}push: true means the source is invoked by an external system (an inbound
webhook or the Event API); push: false means plugsync initiates the call
(a poller, a cron tick, an SFTP listing). fields covers the dotted key
paths accepted inside that source type’s config object, e.g.
inbound.url for a poller — the same config an entry of the config
document’s own sources key carries, since a source’s definition is part of
that document ({"name", "type", "config"} per entry, SourceV3 in
json_schema.$defs). What is NOT in the document, and therefore not
described here, is the state half of a source: its enabled flag, its
secret and its runtime state. See
EventSources for both halves and
the API that manages them.
Credential references are fields like any other. A rest source lists
outbound.credential_name and outbound.secret_headers, the latter a map
of header name to the name of an api_key credential whose value is sent
as that header: the config holds only the credential name, and a dry-run
shows the value as <redacted>. See
Secret static headers.
A poller lists the same keys under inbound (inbound.credential_name,
inbound.default_headers, inbound.secret_headers) for its GET. See
Authenticating the GET.
json_schema
The full JSON Schema (draft 2020-12) of the config document itself:
ConnectorConfigV3.model_json_schema(). This is the root object you PATCH
to a connector’s /draft, with $defs holding every nested model
(EntityV3, FlowV3, StepV3, FieldMapping, the WhenAll/WhenEquals/…
condition variants, and so on):
curl -s https://app.plugsync.com/api/config-surface \
-H "Authorization: Bearer $ORG_API_KEY" | jq '.json_schema | keys, (."$defs" | keys)'Where the schema can express a default value declaratively (a literal, or
one computed from a zero-argument default factory), it does — so a field
you never set but that the server materializes on save (e.g. an empty list
for a collection field) is visible here as "default": [], not just
absent.
plugin_embedded_fields
A flat list of field names, e.g. ["function_arn", "version", "capabilities"], that a published version’s plugin step carries but a
draft never declares by hand. These are resolved and embedded at publish
time (the plugin’s resolved function_arn, its promoted version, its
capability grants); a config-authoring tool that diffs a draft against a
published version should ignore this list rather than reporting it as an
externally-made change on every plugin step.
Validating a config before you push it
json_schema is structural: it accepts or rejects the shape of a config
document (required keys present, types right, enum values in range,
StepV3 fields matching the action’s declared type). It does not encode
the per-action required_checks or limits from the actions/sources
sections above — those live in the runtime validator and are checked at
draft-save or publish time regardless of what a structural check finds.
Two real consumers already validate against this schema before a config reaches the server:
- The in-app config editor compiles an Ajv validator
against
json_schema.$defs.StepV3and flags structural errors on a step as you type it. This check is advisory: the backend’s own validation (includingrequired_checks) is what actually gates a save. - A schema-defaults walker reads
json_schemato tell a locally-edited field apart from one the server fills in with its own default, so a diff against the last published version does not report noise for fields nobody touched.
You can run the same kind of check yourself, against the whole document (not just one step), before you push:
curl -s https://app.plugsync.com/api/config-surface \
-H "Authorization: Bearer $ORG_API_KEY" | jq .json_schema > config-v3.schema.jsonimport json
import jsonschema
schema = json.load(open("config-v3.schema.json"))
config = json.load(open("my-connector.json"))
jsonschema.validate(config, schema) # raises jsonschema.ValidationError with the offending pathA clean run here only means the document is well-formed; it is not a
substitute for the real preflight (GET .../preflight, or
plugsync validate), which is what the backend actually gates a publish
on and is the only place required_checks/limits are enforced.
Out of scope
- Field-by-field prose documentation of each action, transform, source type
and
whencondition: that’s step-actions.md, entities.md and EventSources. - The draft/publish workflow itself: see draft-publish.md.