{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yougrow.ai/developers/schema/batch-request.json",
  "title": "Batch request",
  "description": "The body of POST /api/v2/users/batch: 1–100 users, each a userId plus the same fields as a PATCH. At most 512 KB.",
  "type": "object",
  "properties": {
    "users": {
      "minItems": 1,
      "maxItems": 100,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "description": "Your own id for the user, ≤ 256 chars — the same one everywhere. In a URL, encode it (`encodeURIComponent`). `batch`, `.` and `..` can't be used."
          },
          "email": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 254,
                "format": "email",
                "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
              },
              {
                "type": "null"
              }
            ],
            "description": "Their email address, ≤ 254 chars. We can't email anyone without one. An invalid value is left as it was and listed in `ignoredFields`, not a 400."
          },
          "firstName": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 100
              },
              {
                "type": "null"
              }
            ],
            "description": "Used in greetings (falls back to “there”), ≤ 100 chars. An invalid value is left as it was and listed in `ignoredFields`, not a 400."
          },
          "lastName": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 100
              },
              {
                "type": "null"
              }
            ],
            "description": "≤ 100 chars. An invalid value is left as it was and listed in `ignoredFields`, not a 400."
          },
          "timezone": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 64
              },
              {
                "type": "null"
              }
            ],
            "description": "An IANA time zone, e.g. `Europe/London`. Emails go out in each person's morning; without one, in your connection's default time zone. An invalid value is left as it was and listed in `ignoredFields`, not a 400."
          },
          "locale": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 16,
                "pattern": "^[A-Za-z]{2,3}([-_][A-Za-z0-9]{2,8})*$"
              },
              {
                "type": "null"
              }
            ],
            "description": "A BCP 47 locale, e.g. `en-GB`. An invalid value is left as it was and listed in `ignoredFields`, not a 400."
          },
          "signedUpAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
            "description": "When the account was created: ISO 8601 with a timezone. It starts sign-up journeys while the person is inside the journey's window (72 hours by default), so sending past users with their real date is safe. It can be changed, not cleared."
          },
          "consent": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "consent",
                  "soft_opt_in",
                  "corporate_subscriber",
                  "none"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Your legal basis for marketing email: `consent`, `soft_opt_in`, `corporate_subscriber` or `none`. Service emails (like a welcome) don't need one; a marketing email that's due without one is skipped. `corporate_subscriber` counts as `none` for free-mail addresses (gmail.com, …)."
          },
          "subscribed": {
            "type": "boolean",
            "description": "`false`: they opted out of this email in your product, so they get no more lifecycle email. `true` lifts only that opt-out — never an unsubscribe made in our emails."
          },
          "excluded": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  }
                },
                "required": [
                  "reason"
                ],
                "additionalProperties": false
              },
              {
                "type": "null"
              }
            ],
            "description": "`{\"reason\": \"staff\"}` (reason ≤ 64 chars): never email this person, in any journey — staff, test accounts, invited teammates. `null` lets them into journeys that start from then on."
          },
          "steps": {
            "type": "object",
            "propertyNames": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
            },
            "additionalProperties": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date-time",
                  "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                },
                {
                  "type": "null"
                }
              ]
            },
            "maxProperties": 50,
            "description": "Onboarding step id → when it was done (ISO 8601), or `null` for not done. Use your catalog's ids: lower-case letters, digits, `_` and `-`. Merged key by key; at most 50."
          },
          "facts": {
            "type": "object",
            "propertyNames": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            },
            "additionalProperties": {
              "anyOf": [
                {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 200
                    },
                    {
                      "type": "number"
                    },
                    {
                      "type": "boolean"
                    }
                  ]
                },
                {
                  "type": "null"
                }
              ]
            },
            "maxProperties": 50,
            "description": "Fact id → its latest value: a number, a string (≤ 200 chars) or a boolean. A fact your catalog marks as a date is a string: ISO 8601 with a timezone (`2026-10-08T09:12:00Z`) or a day (`2026-10-08`); one that isn't a date is left as it was and listed in `ignoredFields`. `null` removes one. Leave out one they don't have (a feature not set up, a score not measured) rather than sending 0. Merged key by key; at most 50."
          },
          "traits": {
            "type": "object",
            "propertyNames": {
              "type": "string",
              "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
            },
            "additionalProperties": {
              "anyOf": [
                {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 500
                    },
                    {
                      "type": "number"
                    },
                    {
                      "type": "boolean"
                    }
                  ]
                },
                {
                  "type": "null"
                }
              ]
            },
            "maxProperties": 50,
            "description": "Anything else journeys can branch on, e.g. `plan`: a string (≤ 500 chars), number or boolean. `null` removes one. Keys start with a letter, then letters, digits, `_` or `-`. Email, names, timezone and locale are fields of their own, so they're refused here. Merged key by key; at most 50."
          },
          "entities": {
            "type": "object",
            "propertyNames": {
              "type": "string",
              "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$"
            },
            "additionalProperties": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "kind": {
                      "type": "string",
                      "pattern": "^[a-z][a-z0-9_]{0,39}$"
                    },
                    "name": {
                      "anyOf": [
                        {
                          "type": "string",
                          "maxLength": 120
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "parentId": {
                      "anyOf": [
                        {
                          "type": "string",
                          "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "role": {
                      "anyOf": [
                        {
                          "type": "string",
                          "enum": [
                            "owner",
                            "member",
                            "invited"
                          ]
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "steps": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string",
                        "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
                      },
                      "additionalProperties": {
                        "anyOf": [
                          {
                            "type": "string",
                            "format": "date-time",
                            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "maxProperties": 50
                    },
                    "facts": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 64
                      },
                      "additionalProperties": {
                        "anyOf": [
                          {
                            "anyOf": [
                              {
                                "type": "string",
                                "maxLength": 200
                              },
                              {
                                "type": "number"
                              },
                              {
                                "type": "boolean"
                              }
                            ]
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "maxProperties": 50
                    },
                    "activeAt": {
                      "anyOf": [
                        {
                          "type": "string",
                          "format": "date-time",
                          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  },
                  "additionalProperties": false
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "The things this person has several of — workspaces, brands, projects — by your id: `kind` (your word for it, e.g. `brand`; required the first time), `name`, `parentId`, `role` (`owner`, `member` or `invited`), `steps`, `facts` and `activeAt`. Each merges like the user; `null` removes one. At most 50. Journeys choose which of them each email is about."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
            "description": "Optional: when you read this state (ISO 8601). A write older than the newest one applied is ignored (`stale_write`)."
          }
        },
        "required": [
          "userId"
        ],
        "additionalProperties": false
      }
    }
  },
  "required": [
    "users"
  ],
  "additionalProperties": false
}
