Skip to main content

Inbound webhooks: schemas

Everything a webhook schema can contain: steps or flat fields, every field type and its limits, conditions, defaults and templates.

The schema of a webhook is the contract: it lists the fields a payload may have, their types and their limits. An admin writes it in the admin panel, which also offers templates and checks it as they type. This page is the reference for what a schema can contain. If you only send data, you need it to know what a webhook expects.

Two shapes

A schema has version: 1 and then either steps or fields, never both.

ShapeUse it forThe payload
stepsA form with several parts, such as identity, background and motivation.An object keyed by step, each holding that step's fields.
fieldsA flat payload with no steps, such as an event or any non-form data.The fields directly at the top.
PropertyMeaning
versionschema format version, currently 1
stepsthe steps of a form, in order
fieldsthe fields of a flat payload with no steps, for an event or any non-form data

A form with steps

json
{
  "version": 1,
  "steps": [
    {
      "key": "identity",
      "fields": [
        { "key": "firstname", "type": "string", "required": true, "maxLength": 80, "trim": true },
        { "key": "email", "type": "email", "required": true },
        {
          "key": "status",
          "type": "enum",
          "required": true,
          "options": [
            { "value": "student", "label": "Student" },
            { "value": "professional", "label": "Professional" }
          ]
        }
      ]
    },
    {
      "key": "background",
      "fields": [
        {
          "key": "university",
          "type": "string",
          "required": true,
          "maxLength": 120,
          "condition": { "op": "eq", "field": "identity.status", "value": "student" }
        }
      ]
    }
  ]
}

The payload that matches it:

json
{
  "identity": { "firstname": "Amina", "email": "amina@usthb.dz", "status": "student" },
  "background": { "university": "USTHB" }
}

A step has these properties:

PropertyMeaning
keythe name of this step in the payload
fieldsthe fields of this step
titlehuman-friendly name, shown in the docs
descriptionhelp text, shown in the docs
conditionskip this whole step unless the condition holds

A flat payload

json
{
  "version": 1,
  "fields": [
    { "key": "memberId", "type": "string", "required": true, "maxLength": 32 },
    {
      "key": "plan",
      "type": "enum",
      "required": false,
      "options": [
        { "value": "free", "label": "Free" },
        { "value": "pro", "label": "Pro" }
      ]
    }
  ]
}
json
{ "memberId": "42", "plan": "pro" }

Fields

Every field has these properties, whatever its type. key and type are required, and so is required: a field without an explicit true or false is refused when the admin saves the schema, with MISSING_REQUIRED_FLAG.

PropertyMeaning
keythe name of this field in the payload
typewhat kind of value this field holds
requiredtrue if a value must be sent
labelhuman-friendly name, shown in the docs
descriptionhelp text, shown in the docs
conditiononly ask for this field when the condition holds
defaultvalue used when none is sent

A key starts with a letter or underscore, continues with letters, digits and underscores, and is at most 64 characters. Keys are unique within their step or object.

Field types

TypeWhat it holds
stringshort text, with optional length, pattern and trimming
textlong free text
numbera number, optionally whole and within a range
booleantrue or false
emaila validated address, optionally limited to some domains
urla validated web address
phonea phone number
datea calendar date, YYYY-MM-DD
datetimean ISO 8601 date and time with a timezone
enumone value from a list of options
multi_enumseveral values from a list of options
objecta group of fields
arraya repeatable item, needs maxItems
fileone uploaded file
filesseveral uploaded files, needs maxCount
jsonany JSON value, needs maxBytes

What each type accepts

TypePropertyMeaning
stringminLengthfewest characters allowed
stringmaxLengthmost characters allowed
stringpatternregular expression the value must match
stringtrimstrip spaces around the value first
textmaxLengthmost characters allowed
numberminsmallest value allowed
numbermaxlargest value allowed
numberintegertrue to allow whole numbers only
emailallowedDomainslist of domains the address must belong to
urlallowedSchemeslist of allowed schemes: "http", "https"
phoneregioncountry code the number is for
dateminearliest date, YYYY-MM-DD
datemaxlatest date, YYYY-MM-DD
datetimeminearliest moment, ISO 8601 with timezone
datetimemaxlatest moment, ISO 8601 with timezone
enumoptionslist of { value, label } choices
multi_enumoptionslist of { value, label } choices
multi_enumminSelectedfewest choices to select
multi_enummaxSelectedmost choices to select
objectfieldsthe fields of the group
arrayitemthe field repeated for each entry
arrayminItemsfewest entries allowed
arraymaxItemsmost entries allowed (required)
fileacceptlist of allowed MIME types
filemaxSizeByteslargest file size in bytes
filesacceptlist of allowed MIME types
filesmaxSizeByteslargest size of each file in bytes
filesminCountfewest files allowed
filesmaxCountmost files allowed (required)
jsonmaxByteslargest size in bytes

Notes on a few types:

  • string applies trim before checking lengths and the pattern. A pattern is a regular expression of at most 200 characters, and MCDI stops evaluating one that takes longer than 50 ms.
  • email has a practical format check and a maximum of 254 characters. With allowedDomains, other domains give DOMAIN_NOT_ALLOWED.
  • phone accepts an optional leading + then 6 to 20 digits, spaces, brackets and hyphens.
  • date is YYYY-MM-DD. datetime is ISO 8601 and must include a timezone, for example 2026-01-31T09:00:00Z.
  • url with allowedSchemes of ["https"] refuses http addresses.
  • enum and multi_enum take options, each with a value and a label. Only the value is sent in a payload.

Option properties:

PropertyMeaning
valuethe value sent in the payload
labelhuman-friendly name, shown in the docs

Objects and arrays

An object holds a group of fields. An array repeats one item field and needs maxItems, so a payload cannot be unbounded. Here a team lists up to five members, and each member's github address is asked only of developers:

json
{
  "version": 1,
  "fields": [
    { "key": "team", "type": "string", "required": true, "maxLength": 60, "trim": true },
    {
      "key": "members",
      "type": "array",
      "required": true,
      "minItems": 1,
      "maxItems": 5,
      "item": {
        "key": "member",
        "type": "object",
        "required": true,
        "fields": [
          { "key": "name", "type": "string", "required": true, "maxLength": 80 },
          {
            "key": "role",
            "type": "enum",
            "required": true,
            "options": [
              { "value": "dev", "label": "Developer" },
              { "value": "design", "label": "Designer" }
            ]
          },
          {
            "key": "github",
            "type": "url",
            "required": true,
            "allowedSchemes": ["https"],
            "condition": { "op": "eq", "field": "./role", "value": "dev" }
          }
        ]
      }
    },
    { "key": "wantsMentor", "type": "boolean", "required": false, "default": false }
  ]
}
json
{
  "team": "Rocket",
  "members": [
    { "name": "Sara", "role": "dev", "github": "https://github.com/sara" },
    { "name": "Lina", "role": "design" }
  ],
  "wantsMentor": true
}

In an error, array entries are addressed by position, for example members[0].github.

Conditions

A condition makes a field, or a whole step, apply only when something else holds. When it does not hold, the field is ignored: it is not required, and a value sent for it is dropped and not stored. Sending github for a designer in the example above succeeds, and the stored submission has no github.

A condition has an operator, op, and what it needs:

OperatorHolds when the field
eqequals the value
nedoes not equal the value
gtgreater than the value
ltless than the value
gtegreater than or equal to the value
lteless than or equal to the value
inis one of the values in a list
containscontains the value
existshas been given a value
andall of the conditions in `of` hold
orany of the conditions in `of` holds
notthe condition in `of` does not hold
PropertyMeaning
ophow to compare
fieldpath of the field to read, e.g. "identity.status", or "./role" for the same list entry
valuewhat to compare it with
ofthe conditions combined by and / or / not

The field is the path of the field to read. In a form with steps it is step.field, as in identity.status. In a flat schema it is just the key. Inside an array item, ./role means the role of the same entry, which is how each team member's github depends on their own role. A condition can only refer to a field that comes earlier, so a forward reference is refused when the schema is saved.

Combine conditions with and, or and not, which list their conditions in of:

json
{
  "op": "and",
  "of": [
    { "op": "eq", "field": "identity.status", "value": "student" },
    { "op": "exists", "field": "identity.email" }
  ]
}

Defaults and unknown fields

A field with a default takes that value when none is sent, and the default is stored: the first example submission above that omitted wantsMentor was stored with "wantsMentor": false. With "reject unknown fields" on, which is the default, any key the schema does not declare gives UNKNOWN_FIELD, or UNKNOWN_STEP for a stray top-level key in a form with steps.

Limits

LimitValue
Steps20
Fields in the whole schema200
Nesting depth of objects and arrays5
Length of a field key64 characters
Length of a pattern200 characters

A schema over a limit is refused when the admin saves it, with TOO_MANY_STEPS, TOO_MANY_NODES, TOO_DEEP, KEY_TOO_LONG or PATTERN_TOO_LONG. The payload size is limited too: the API accepts a request body up to 512 KB.

Templates

The admin panel starts a new webhook from one of these. They are real schemas you can use as they are or change.

TemplateNameWhat it is
recruitmentRecruitmentThree steps: identity, a background that depends on the status, and motivation.
workshopWorkshop sign-upOne step: who is coming, their level and topics, and consent.
eventEvent registrationAttendee details plus a list of guests, where a guest can be a member.
project-eventProject eventNo steps: a flat payload that describes something that happened in the project.
blankBlankOne step with one field, to start from.

When a schema is refused

When an admin saves a schema that breaks a rule, the API answers 400 with every problem and its position:

json
{
  "statusCode": 400,
  "error": "InvalidSchema",
  "errors": [
    {
      "path": "fields[1].required",
      "code": "MISSING_REQUIRED_FLAG",
      "message": "`required` must be a boolean"
    }
  ]
}

The codes you are most likely to see are MISSING_REQUIRED_FLAG, MISSING_MAX_ITEMS, MISSING_MAX_COUNT, MISSING_MAX_SIZE, MISSING_MAX_BYTES (a limit the type needs), NO_OPTIONS, DUPLICATE_KEY, INVALID_PATTERN, FORWARD_REFERENCE and UNKNOWN_PROPERTY (a property the type does not have).

The tables on this page come straight from the shared contracts package that the admin editor also uses, so they list exactly the types and properties the editor offers. The API's validator is the final authority.

Source: packages/contracts/src/inbound-webhooks.ts, apps/api/src/modules/inbound-webhooks/schema/schema.validator.ts, apps/api/src/modules/inbound-webhooks/schema/payload.validator.ts, apps/api/src/modules/inbound-webhooks/schema/form-schema.types.ts, apps/api/src/modules/inbound-webhooks/schema/condition.evaluator.ts.