{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yougrow.ai/developers/schema/user-patch.json",
  "title": "User patch",
  "description": "The body of PATCH /api/v2/users/{userId}: any subset of a user's state, as a JSON Merge Patch. At most 512 KB.",
  "type": "object",
  "properties": {
    "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`)."
    }
  },
  "additionalProperties": false
}
