{
  "openapi": "3.1.0",
  "info": {
    "title": "YouGrow API",
    "version": "2.0.0",
    "summary": "Send your users' state to YouGrow lifecycle journeys.",
    "description": "Your server sends YouGrow each user's **current state** — who they are, when they signed up, which onboarding steps they've done, facts about their account, and whether they've opted out. YouGrow keeps it and decides the rest: who enters which journey, what to send and when.\n\n- **Authentication:** HTTP Basic over HTTPS — your key id (`ygk_…`) as the username and your secret (`ygs_…`) as the password. After you rotate the secret, the previous one works for 24 hours.\n- **Writes are JSON Merge Patches** (RFC 7396): fields you send replace ours, fields you leave out stay, `null` clears one, and `steps`, `facts` and `traits` merge key by key. Sending the same state twice is harmless, so any write can be retried.\n- **Out-of-order writes:** send `updatedAt` (when you read the state), and a write older than the newest one applied is skipped: `200 {\"applied\": false, \"reason\": \"stale_write\"}` — a success, not a conflict.\n- **Limits:** 600 requests a minute and 20,000 an hour per key (a batch counts as one), 512 KB per request, 100 users per batch.\n- **Errors:** a `400` lists each problem in `fields` — fix it rather than retry. Retry a `429` after `Retry-After`, and a `5xx` or a network error with backoff. Where API v2 isn't switched on, every endpoint answers `404 api_disabled`.\n- **What YouGrow sends you** — the context request and webhooks — is under Webhooks. Each carries an ES256 JWT for you to verify.\n\nGuides and examples: https://yougrow.ai/developers. API v1 (`POST /api/v1/events`, HMAC-signed) was removed on 2026-09-25."
  },
  "externalDocs": {
    "description": "Developer docs",
    "url": "https://yougrow.ai/developers/users"
  },
  "servers": [
    {
      "url": "https://yougrow.ai"
    }
  ],
  "security": [
    {
      "basicAuth": []
    }
  ],
  "tags": [
    {
      "name": "Users",
      "description": "Send each user's current state, read it back, or erase it."
    },
    {
      "name": "Events",
      "description": "Optional milestones."
    },
    {
      "name": "Connection",
      "description": "Check the key you're using."
    }
  ],
  "paths": {
    "/api/v2/users/{userId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/UserId"
        }
      ],
      "patch": {
        "operationId": "updateUser",
        "tags": [
          "Users"
        ],
        "summary": "Send a user's state",
        "description": "Merge a JSON Merge Patch (RFC 7396) into the user's state: fields you send replace ours, fields you leave out stay, `null` clears one, and `steps`, `facts` and `traits` merge key by key. The first write for an id creates the user. `signedUpAt` enrols them in sign-up journeys while they're inside the journey's window — checked on every write, so a retry or a late write still enrols, and when a journey goes live YouGrow enrols everyone still inside it. An invalid `email`, `firstName`, `lastName`, `timezone` or `locale` doesn't sink the write: it's left as it was and listed in `ignoredFields`. Sending the same state twice is harmless.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserPatch"
              },
              "examples": {
                "signUp": {
                  "summary": "At sign-up",
                  "value": {
                    "signedUpAt": "2026-09-25T09:30:00Z",
                    "email": "alex@example.com",
                    "firstName": "Alex",
                    "timezone": "Europe/London",
                    "locale": "en-GB",
                    "consent": "soft_opt_in",
                    "traits": {
                      "plan": "pro"
                    }
                  }
                },
                "progress": {
                  "summary": "Steps and facts, from a scheduled sync",
                  "value": {
                    "steps": {
                      "create_project": "2026-09-25T10:02:00Z"
                    },
                    "facts": {
                      "projects": 1
                    },
                    "updatedAt": "2026-09-25T10:05:00Z"
                  }
                },
                "optOut": {
                  "summary": "Opted out in your product",
                  "value": {
                    "subscribed": false
                  }
                },
                "exclude": {
                  "summary": "Never email this person",
                  "value": {
                    "excluded": {
                      "reason": "staff"
                    }
                  }
                },
                "clear": {
                  "summary": "Clear a field, a step and a trait",
                  "value": {
                    "lastName": null,
                    "steps": {
                      "invite_team": null
                    },
                    "traits": {
                      "role": null
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied — or skipped because YouGrow holds something newer (`applied: false`). Neither needs a retry. A skipped write is a 200, not a 409.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatchResponse"
                },
                "examples": {
                  "applied": {
                    "summary": "Applied",
                    "value": {
                      "applied": true,
                      "user": {
                        "userId": "user_123",
                        "email": "alex@example.com",
                        "firstName": "Alex",
                        "lastName": null,
                        "timezone": "Europe/London",
                        "locale": "en-GB",
                        "signedUpAt": "2026-09-25T09:30:00.000Z",
                        "consent": "soft_opt_in",
                        "subscribed": true,
                        "excluded": null,
                        "steps": {
                          "create_project": "2026-09-25T10:02:00.000Z"
                        },
                        "facts": {
                          "projects": 1
                        },
                        "traits": {
                          "plan": "pro"
                        },
                        "entities": {},
                        "updatedAt": "2026-09-25T10:05:00.000Z"
                      }
                    }
                  },
                  "fieldsIgnored": {
                    "summary": "Applied, with an invalid timezone left as it was",
                    "value": {
                      "applied": true,
                      "user": {
                        "userId": "user_123",
                        "email": "alex@example.com",
                        "firstName": "Alex",
                        "lastName": null,
                        "timezone": "Europe/London",
                        "locale": "en-GB",
                        "signedUpAt": "2026-09-25T09:30:00.000Z",
                        "consent": "soft_opt_in",
                        "subscribed": true,
                        "excluded": null,
                        "steps": {
                          "create_project": "2026-09-25T10:02:00.000Z"
                        },
                        "facts": {
                          "projects": 1
                        },
                        "traits": {
                          "plan": "pro"
                        },
                        "entities": {},
                        "updatedAt": "2026-09-25T10:05:00.000Z"
                      },
                      "ignoredFields": [
                        {
                          "path": "timezone",
                          "message": "not an IANA time zone, e.g. Europe/London"
                        }
                      ]
                    }
                  },
                  "stale": {
                    "summary": "Skipped: older than the stored state",
                    "value": {
                      "applied": false,
                      "reason": "stale_write",
                      "storedUpdatedAt": "2026-09-25T10:05:00.000Z",
                      "user": {
                        "userId": "user_123",
                        "email": "alex@example.com",
                        "firstName": "Alex",
                        "lastName": null,
                        "timezone": "Europe/London",
                        "locale": "en-GB",
                        "signedUpAt": "2026-09-25T09:30:00.000Z",
                        "consent": "soft_opt_in",
                        "subscribed": true,
                        "excluded": null,
                        "steps": {
                          "create_project": "2026-09-25T10:02:00.000Z"
                        },
                        "facts": {
                          "projects": 1
                        },
                        "traits": {
                          "plan": "pro"
                        },
                        "entities": {},
                        "updatedAt": "2026-09-25T10:05:00.000Z"
                      }
                    }
                  },
                  "deleted": {
                    "summary": "Skipped: older than a later DELETE",
                    "value": {
                      "applied": false,
                      "reason": "deleted_later",
                      "storedUpdatedAt": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "5XX": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "get": {
        "operationId": "getUser",
        "tags": [
          "Users"
        ],
        "summary": "Read a user back",
        "description": "The state YouGrow holds, plus the journeys they're in and any opt-outs made in YouGrow's emails. 404 when YouGrow doesn't hold them.",
        "responses": {
          "200": {
            "description": "The user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserView"
                },
                "examples": {
                  "user": {
                    "summary": "A user in a journey",
                    "value": {
                      "userId": "user_123",
                      "email": "alex@example.com",
                      "firstName": "Alex",
                      "lastName": null,
                      "timezone": "Europe/London",
                      "locale": "en-GB",
                      "signedUpAt": "2026-09-25T09:30:00.000Z",
                      "consent": "soft_opt_in",
                      "subscribed": true,
                      "excluded": null,
                      "steps": {
                        "create_project": "2026-09-25T10:02:00.000Z"
                      },
                      "facts": {
                        "projects": 1
                      },
                      "traits": {
                        "plan": "pro"
                      },
                      "entities": {},
                      "updatedAt": "2026-09-25T10:05:00.000Z",
                      "enrolments": [
                        {
                          "journeyId": "lcj_4f8a2c",
                          "status": "active",
                          "mode": "live",
                          "enrolledAt": "2026-09-25T09:30:05.000Z"
                        }
                      ],
                      "optOuts": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "5XX": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "deleteUser",
        "tags": [
          "Users"
        ],
        "summary": "Erase a user",
        "description": "Erases the profile, its events, journey progress, AI drafts and invite links. Opt-outs are kept with the email address removed — a one-way hash still matches it — so someone who unsubscribed stays unsubscribed. For 30 days a tombstone keeps only a one-way hash of your connection and user ids and the time of deletion: a later write starts a fresh person, unless its `updatedAt` is older than the deletion (`deleted_later`). Always 204, even for a user YouGrow never saw, so it's safe to repeat.",
        "responses": {
          "204": {
            "description": "Erased (or nothing to erase)."
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "5XX": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v2/users/batch": {
      "post": {
        "operationId": "updateUsers",
        "tags": [
          "Users"
        ],
        "summary": "Send many users' state",
        "description": "1–100 users, each a `userId` plus the same fields as a PATCH. Each is checked and applied on its own — never all-or-nothing — so one bad item never fails the rest. A batch counts as one request against the rate limit.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              },
              "examples": {
                "sync": {
                  "summary": "A scheduled sync",
                  "value": {
                    "users": [
                      {
                        "userId": "user_123",
                        "steps": {
                          "create_project": "2026-09-25T10:02:00Z"
                        },
                        "updatedAt": "2026-09-25T10:05:00Z"
                      },
                      {
                        "userId": "user_456",
                        "facts": {
                          "projects": 0
                        },
                        "updatedAt": "2026-09-25T10:00:00Z"
                      },
                      {
                        "userId": "user_789",
                        "timezone": "Europe/Paris"
                      },
                      {
                        "userId": "user_999",
                        "subscribed": false
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Every item was tried. `results` lists the ones that weren't applied, and applied ones with `fields_ignored`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchResponse"
                },
                "examples": {
                  "mixed": {
                    "summary": "Two applied (one with a field ignored), one ignored, one failed",
                    "value": {
                      "applied": 2,
                      "ignored": 1,
                      "failed": 1,
                      "results": [
                        {
                          "index": 1,
                          "userId": "user_456",
                          "status": "ignored",
                          "reason": "stale_write"
                        },
                        {
                          "index": 2,
                          "userId": "user_789",
                          "status": "applied",
                          "reason": "fields_ignored",
                          "fields": [
                            {
                              "path": "timezone",
                              "message": "not an IANA time zone, e.g. Europe/London"
                            }
                          ]
                        },
                        {
                          "index": 3,
                          "userId": "user_999",
                          "status": "failed",
                          "reason": "invalid",
                          "fields": [
                            {
                              "path": "subscribed",
                              "message": "Invalid input: expected boolean, received string"
                            }
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body isn't `{\"users\": [...]}` with 1–100 items. (A bad item is reported in `results`, not here.)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "5XX": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v2/me": {
      "get": {
        "operationId": "getConnection",
        "tags": [
          "Connection"
        ],
        "summary": "Check your key",
        "description": "The connection behind the key: its name, environment and status. Call it to check your credentials, and that they're the right environment's, before you send anything. It counts against the rate limit like any request.",
        "responses": {
          "200": {
            "description": "The key works.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeResponse"
                },
                "examples": {
                  "production": {
                    "summary": "A production connection",
                    "value": {
                      "connection": {
                        "id": "pcn_7d2f19",
                        "name": "Acme",
                        "environment": "production",
                        "status": "active"
                      },
                      "keyId": "ygk_3f9a2b7c",
                      "rotating": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "5XX": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v2/users/{userId}/events": {
      "parameters": [
        {
          "$ref": "#/components/parameters/UserId"
        }
      ],
      "post": {
        "operationId": "trackEvent",
        "tags": [
          "Events"
        ],
        "summary": "Record a milestone",
        "description": "Optional: journeys run on state. Record a moment that matters in itself, and a journey can start from it or branch on it. The user must exist — send their state first. Sign-ups, steps, opt-outs and deletion are state, so their v1 event names are refused.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventRequest"
              },
              "examples": {
                "milestone": {
                  "summary": "A milestone",
                  "value": {
                    "event": "report.exported",
                    "properties": {
                      "format": "pdf"
                    },
                    "occurredAt": "2026-09-25T10:15:00Z",
                    "idempotencyKey": "export_8812"
                  }
                },
                "done": {
                  "summary": "Finished getting started (no step checklist)",
                  "value": {
                    "event": "onboarding.completed"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded — or already recorded with this `idempotencyKey`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventResponse"
                },
                "examples": {
                  "recorded": {
                    "summary": "Recorded",
                    "value": {
                      "recorded": true,
                      "duplicate": false
                    }
                  },
                  "duplicate": {
                    "summary": "A repeat of the same idempotencyKey",
                    "value": {
                      "recorded": false,
                      "duplicate": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "5XX": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    }
  },
  "webhooks": {
    "contextRequest": {
      "post": {
        "operationId": "contextRequest",
        "summary": "Context request",
        "description": "Only while you've set a context endpoint: just before a journey acts for a user, YouGrow asks your server for their live state. Answer within your timeout (2 seconds by default, 5 at most). If it fails 3 times in a row, YouGrow uses the stored state for 15 minutes before asking again. Verify the bearer token before trusting anything: ES256, a key from https://yougrow.ai/.well-known/jwks.json, `iss` = `https://yougrow.ai`, `aud` = your key id, `dir`, `exp`, and `body_sha256` = the base64url SHA-256 of the raw body. `dir` is `context`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "X-YouGrow-Key-Id",
            "in": "header",
            "required": true,
            "description": "Your connection's key id — the token's audience.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContextRequest"
              },
              "examples": {
                "send": {
                  "summary": "Before an email",
                  "value": {
                    "userId": "user_123",
                    "purpose": "send",
                    "journeyId": "lcj_4f8a2c",
                    "nodeId": "email_2",
                    "entity": {
                      "id": "proj_42",
                      "kind": "project"
                    },
                    "requestId": "b5c1e6d2-8a0f-4c1e-9d55-3f1a2b7c9e10"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The user's live state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContextResponse"
                },
                "examples": {
                  "live": {
                    "summary": "Steps, facts and an insight",
                    "value": {
                      "asOf": "2026-09-25T10:00:00Z",
                      "steps": [
                        {
                          "id": "create_project",
                          "label": "Create a project",
                          "done": true,
                          "doneAt": "2026-09-25T10:02:00Z"
                        },
                        {
                          "id": "invite_team",
                          "label": "Invite your team",
                          "done": false,
                          "url": "https://app.example.com/team/invite"
                        }
                      ],
                      "nextStep": {
                        "id": "invite_team",
                        "label": "Invite your team",
                        "url": "https://app.example.com/team/invite"
                      },
                      "facts": [
                        {
                          "id": "projects",
                          "label": "Projects",
                          "value": 1
                        }
                      ],
                      "insights": [
                        {
                          "id": "first_project",
                          "sentence": "Your first project is set up and ready for your team.",
                          "factIds": [
                            "projects"
                          ],
                          "supportsStep": "invite_team"
                        }
                      ]
                    }
                  },
                  "exit": {
                    "summary": "Stop email for this user",
                    "value": {
                      "asOf": "2026-09-25T10:00:00Z",
                      "exit": {
                        "reason": "staff account"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The token didn't verify."
          },
          "404": {
            "description": "You don't know this user."
          }
        }
      }
    },
    "webhook": {
      "post": {
        "operationId": "webhook",
        "summary": "Webhook",
        "description": "Changes your product should mirror: `email_preferences.updated` (an unsubscribe from one of YouGrow's emails), `email.suppressed` (YouGrow stopped emailing a user because their address hard-bounced or they reported an email as spam) and `connection.test` (the Test webhook button). API writes never trigger one. Reply 2xx within 5 seconds; anything else is retried with backoff for 24 hours, with the same `id`. More types may be added: reply 2xx to any you don't handle. Verify the bearer token before trusting anything: ES256, a key from https://yougrow.ai/.well-known/jwks.json, `iss` = `https://yougrow.ai`, `aud` = your key id, `dir`, `exp`, and `body_sha256` = the base64url SHA-256 of the raw body. `dir` is `webhook`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "X-YouGrow-Key-Id",
            "in": "header",
            "required": true,
            "description": "Your connection's key id — the token's audience.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              },
              "examples": {
                "unsubscribe": {
                  "summary": "An unsubscribe",
                  "value": {
                    "id": "wh_7c1d9e",
                    "type": "email_preferences.updated",
                    "createdAt": "2026-09-25T10:00:00Z",
                    "data": {
                      "userId": "user_123",
                      "category": "onboarding",
                      "subscribed": false,
                      "scope": "category",
                      "source": "list-unsubscribe"
                    }
                  }
                },
                "suppressed": {
                  "summary": "A spam complaint",
                  "value": {
                    "id": "wh_2b8e4f",
                    "type": "email.suppressed",
                    "createdAt": "2026-09-25T10:00:00Z",
                    "data": {
                      "userId": "user_123",
                      "reason": "complaint"
                    }
                  }
                },
                "test": {
                  "summary": "Test webhook",
                  "value": {
                    "id": "wh_test_1",
                    "type": "connection.test",
                    "createdAt": "2026-09-25T10:00:00Z",
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The token didn't verify."
          },
          "2XX": {
            "description": "Received."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "UserPatch": {
        "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": {
            "$ref": "#/components/schemas/Timestamp",
            "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": [
                {
                  "$ref": "#/components/schemas/Timestamp"
                },
                {
                  "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": [
                          {
                            "$ref": "#/components/schemas/Timestamp"
                          },
                          {
                            "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": [
                        {
                          "$ref": "#/components/schemas/Timestamp"
                        },
                        {
                          "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": {
            "$ref": "#/components/schemas/Timestamp",
            "description": "Optional: when you read this state (ISO 8601). A write older than the newest one applied is ignored (`stale_write`)."
          }
        },
        "additionalProperties": false,
        "description": "Any subset of a user's state, as a JSON Merge Patch (RFC 7396): fields you send replace ours, fields you leave out stay, `null` clears one, and `steps`, `facts` and `traits` merge key by key. Unknown fields are refused."
      },
      "BatchItem": {
        "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": {
            "$ref": "#/components/schemas/Timestamp",
            "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": [
                {
                  "$ref": "#/components/schemas/Timestamp"
                },
                {
                  "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": [
                          {
                            "$ref": "#/components/schemas/Timestamp"
                          },
                          {
                            "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": [
                        {
                          "$ref": "#/components/schemas/Timestamp"
                        },
                        {
                          "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": {
            "$ref": "#/components/schemas/Timestamp",
            "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,
        "description": "One user in a batch: their id plus the same fields as a PATCH."
      },
      "BatchRequest": {
        "type": "object",
        "properties": {
          "users": {
            "minItems": 1,
            "maxItems": 100,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchItem"
            }
          }
        },
        "required": [
          "users"
        ],
        "additionalProperties": false,
        "description": "1–100 users. Each is checked and applied on its own, so one bad item never fails the rest."
      },
      "EventRequest": {
        "type": "object",
        "properties": {
          "event": {
            "type": "string",
            "maxLength": 80,
            "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$",
            "description": "Lower-case, dot-separated, ≤ 80 chars, e.g. `report.exported`. `user.signed_up`, `onboarding.step_completed`, `user.marketing_consent_granted`, `entity.created`, `user.deleted` and `email_preferences.updated` are refused: send `signedUpAt`, `steps`, `consent`, `entities` or `subscribed`, or DELETE the user. `onboarding.completed` marks the user activated."
          },
          "properties": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {},
            "description": "Anything useful, ≤ 4 KB serialised."
          },
          "occurredAt": {
            "$ref": "#/components/schemas/Timestamp",
            "description": "When it happened (ISO 8601 with a timezone). Defaults to now."
          },
          "idempotencyKey": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "Makes a retry harmless: the same key is recorded once."
          },
          "entityId": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
            "description": "The entity it happened to — one of the user's `entities`, e.g. the brand an audit ran on. A journey about one entity follows it."
          }
        },
        "required": [
          "event"
        ],
        "additionalProperties": false,
        "description": "An optional milestone that journeys can start from or branch on."
      },
      "ContextResponse": {
        "type": "object",
        "properties": {
          "asOf": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "steps": {
            "default": [],
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
                },
                "label": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 120
                },
                "done": {
                  "type": "boolean"
                },
                "doneAt": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/Timestamp"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "url": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 2000
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "blocked": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 200
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              },
              "required": [
                "id",
                "label",
                "done"
              ]
            }
          },
          "nextStep": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
                  },
                  "label": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "url": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 2000
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                },
                "required": [
                  "id",
                  "label"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "facts": {
            "default": [],
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 64
                },
                "label": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 120
                },
                "value": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 200
                    },
                    {
                      "type": "number"
                    },
                    {
                      "type": "boolean"
                    }
                  ]
                },
                "unit": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 20
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "display": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 200
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "source": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 120
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "observedAt": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/Timestamp"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              },
              "required": [
                "id",
                "label",
                "value"
              ]
            }
          },
          "insights": {
            "default": [],
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 64
                },
                "sentence": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 300
                },
                "factIds": {
                  "default": [],
                  "maxItems": 10,
                  "type": "array",
                  "items": {
                    "type": "string",
                    "maxLength": 64
                  }
                },
                "weight": {
                  "default": 0.5,
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                },
                "supportsStep": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 64
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              },
              "required": [
                "id",
                "sentence"
              ]
            }
          },
          "consent": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "basis": {
                    "type": "string",
                    "enum": [
                      "consent",
                      "soft_opt_in",
                      "corporate_subscriber",
                      "none"
                    ]
                  },
                  "categories": {
                    "type": "object",
                    "propertyNames": {
                      "type": "string"
                    },
                    "additionalProperties": {
                      "type": "boolean"
                    }
                  }
                },
                "required": [
                  "basis"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "hold": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "until": {
                    "anyOf": [
                      {
                        "$ref": "#/components/schemas/Timestamp"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 200
                  }
                },
                "required": [
                  "reason"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "exit": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 200
                  }
                },
                "required": [
                  "reason"
                ]
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "asOf"
        ],
        "description": "What your context endpoint returns for one user: at most 64 KB, within your timeout. Only `asOf` is required."
      },
      "Timestamp": {
        "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": "ISO 8601 with a timezone, e.g. `2026-09-25T10:00:00Z`."
      },
      "UserState": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "Your id for the user."
          },
          "email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "As you last sent it; `null` if you haven't."
          },
          "firstName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "As you last sent it; `null` if you haven't."
          },
          "lastName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "As you last sent it; `null` if you haven't."
          },
          "timezone": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "As you last sent it; `null` if you haven't."
          },
          "locale": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "As you last sent it; `null` if you haven't."
          },
          "signedUpAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "When the account was created, in UTC; `null` if you haven't sent it."
          },
          "consent": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "consent",
                  "soft_opt_in",
                  "corporate_subscriber",
                  "none"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "The legal basis you sent."
          },
          "subscribed": {
            "type": "boolean",
            "description": "`false` while they're opted out in your product. Unsubscribes made in our emails are in `optOuts` (GET only)."
          },
          "excluded": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                },
                "required": [
                  "reason"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Set while they're excluded from every journey."
          },
          "steps": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "string"
            },
            "maxProperties": 50,
            "description": "Step id → when it was done, in UTC."
          },
          "facts": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "number"
                },
                {
                  "type": "boolean"
                }
              ]
            },
            "maxProperties": 50,
            "description": "Fact id → the latest value you sent."
          },
          "traits": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 500
                },
                {
                  "type": "number"
                },
                {
                  "type": "boolean"
                }
              ]
            },
            "maxProperties": 50,
            "description": "Trait key → value."
          },
          "entities": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string"
                },
                "name": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "parentId": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "role": {
                  "anyOf": [
                    {
                      "type": "string",
                      "enum": [
                        "owner",
                        "member",
                        "invited"
                      ]
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "steps": {
                  "type": "object",
                  "propertyNames": {
                    "type": "string"
                  },
                  "additionalProperties": {
                    "type": "string"
                  },
                  "maxProperties": 50
                },
                "facts": {
                  "type": "object",
                  "propertyNames": {
                    "type": "string"
                  },
                  "additionalProperties": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 200
                      },
                      {
                        "type": "number"
                      },
                      {
                        "type": "boolean"
                      }
                    ]
                  },
                  "maxProperties": 50
                },
                "activeAt": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              },
              "required": [
                "kind",
                "name",
                "parentId",
                "role",
                "steps",
                "facts",
                "activeAt"
              ]
            },
            "description": "Entity id → its kind, name, parent, role, steps, facts and when they last worked on it."
          },
          "updatedAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The newest `updatedAt` applied, or `null` if you've never sent one."
          }
        },
        "required": [
          "userId",
          "email",
          "firstName",
          "lastName",
          "timezone",
          "locale",
          "signedUpAt",
          "consent",
          "subscribed",
          "excluded",
          "steps",
          "facts",
          "traits",
          "entities",
          "updatedAt"
        ],
        "description": "A user's state as YouGrow holds it."
      },
      "UserView": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "Your id for the user."
          },
          "email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "As you last sent it; `null` if you haven't."
          },
          "firstName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "As you last sent it; `null` if you haven't."
          },
          "lastName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "As you last sent it; `null` if you haven't."
          },
          "timezone": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "As you last sent it; `null` if you haven't."
          },
          "locale": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "As you last sent it; `null` if you haven't."
          },
          "signedUpAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "When the account was created, in UTC; `null` if you haven't sent it."
          },
          "consent": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "consent",
                  "soft_opt_in",
                  "corporate_subscriber",
                  "none"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "The legal basis you sent."
          },
          "subscribed": {
            "type": "boolean",
            "description": "`false` while they're opted out in your product. Unsubscribes made in our emails are in `optOuts` (GET only)."
          },
          "excluded": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                },
                "required": [
                  "reason"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Set while they're excluded from every journey."
          },
          "steps": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "string"
            },
            "maxProperties": 50,
            "description": "Step id → when it was done, in UTC."
          },
          "facts": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "number"
                },
                {
                  "type": "boolean"
                }
              ]
            },
            "maxProperties": 50,
            "description": "Fact id → the latest value you sent."
          },
          "traits": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 500
                },
                {
                  "type": "number"
                },
                {
                  "type": "boolean"
                }
              ]
            },
            "maxProperties": 50,
            "description": "Trait key → value."
          },
          "entities": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string"
                },
                "name": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "parentId": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "role": {
                  "anyOf": [
                    {
                      "type": "string",
                      "enum": [
                        "owner",
                        "member",
                        "invited"
                      ]
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "steps": {
                  "type": "object",
                  "propertyNames": {
                    "type": "string"
                  },
                  "additionalProperties": {
                    "type": "string"
                  },
                  "maxProperties": 50
                },
                "facts": {
                  "type": "object",
                  "propertyNames": {
                    "type": "string"
                  },
                  "additionalProperties": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 200
                      },
                      {
                        "type": "number"
                      },
                      {
                        "type": "boolean"
                      }
                    ]
                  },
                  "maxProperties": 50
                },
                "activeAt": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              },
              "required": [
                "kind",
                "name",
                "parentId",
                "role",
                "steps",
                "facts",
                "activeAt"
              ]
            },
            "description": "Entity id → its kind, name, parent, role, steps, facts and when they last worked on it."
          },
          "updatedAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The newest `updatedAt` applied, or `null` if you've never sent one."
          },
          "enrolments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "journeyId": {
                  "type": "string"
                },
                "status": {
                  "type": "string"
                },
                "mode": {
                  "type": "string"
                },
                "enrolledAt": {
                  "type": "string"
                },
                "entityId": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              },
              "required": [
                "journeyId",
                "status",
                "mode",
                "enrolledAt"
              ]
            },
            "description": "The journeys they're in or have been through: the journey's id, `status` (`active`, `completed` or `exited`), `mode` (`test`, `shadow` or `live`), when they joined, and `entityId` when the journey is about one of their entities."
          },
          "optOuts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "scope": {
                  "type": "string",
                  "enum": [
                    "all",
                    "category"
                  ]
                },
                "category": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "at": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              },
              "required": [
                "scope",
                "category",
                "at"
              ]
            },
            "description": "Unsubscribes made in YouGrow's emails: `scope` `all` or `category` (with the `category`), and when. The API can't lift them, and `subscribed: true` doesn't either."
          }
        },
        "required": [
          "userId",
          "email",
          "firstName",
          "lastName",
          "timezone",
          "locale",
          "signedUpAt",
          "consent",
          "subscribed",
          "excluded",
          "steps",
          "facts",
          "traits",
          "entities",
          "updatedAt",
          "enrolments",
          "optOuts"
        ],
        "description": "A user's state, plus what YouGrow decided: the journeys they're in and their opt-outs."
      },
      "PatchResponse": {
        "anyOf": [
          {
            "type": "object",
            "properties": {
              "applied": {
                "type": "boolean",
                "const": true
              },
              "user": {
                "$ref": "#/components/schemas/UserState"
              },
              "ignoredFields": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/FieldError"
                }
              }
            },
            "required": [
              "applied",
              "user"
            ]
          },
          {
            "type": "object",
            "properties": {
              "applied": {
                "type": "boolean",
                "const": false
              },
              "reason": {
                "type": "string",
                "enum": [
                  "stale_write",
                  "deleted_later"
                ]
              },
              "storedUpdatedAt": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "user": {
                "$ref": "#/components/schemas/UserState"
              }
            },
            "required": [
              "applied",
              "reason"
            ]
          }
        ],
        "description": "`applied: true` — saved; `user` is what we now hold, and `ignoredFields` lists any profile field (`email`, `firstName`, `lastName`, `timezone`, `locale`) whose value was invalid and so left as it was, and any fact your catalog marks as a date whose value wasn't one. `applied: false` — skipped because we hold something newer: `stale_write` (older than the stored `updatedAt`) or `deleted_later` (older than a later DELETE). Neither needs a retry."
      },
      "BatchResponse": {
        "type": "object",
        "properties": {
          "applied": {
            "type": "integer",
            "description": "Users whose state was saved (some perhaps with `fields_ignored`, listed in `results`)."
          },
          "ignored": {
            "type": "integer",
            "description": "Users skipped because we hold something newer (`stale_write` or `deleted_later`). Nothing to do."
          },
          "failed": {
            "type": "integer",
            "description": "Users not saved: `invalid` (see `fields`) — fix and resend — or `internal_error` — resend."
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "userId": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "applied",
                    "ignored",
                    "failed"
                  ]
                },
                "reason": {
                  "type": "string"
                },
                "fields": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/FieldError"
                  }
                }
              },
              "required": [
                "index",
                "userId",
                "status",
                "reason"
              ]
            }
          }
        },
        "required": [
          "applied",
          "ignored",
          "failed",
          "results"
        ],
        "description": "How many users were applied, ignored and failed. `results` lists the items that weren't applied, and applied ones whose invalid profile fields were left as they were (`fields_ignored`); `index` is the item's position in your `users` array."
      },
      "MeResponse": {
        "type": "object",
        "properties": {
          "connection": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "environment": {
                "anyOf": [
                  {
                    "type": "string",
                    "enum": [
                      "staging",
                      "production"
                    ]
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "paused",
                  "revoked"
                ]
              }
            },
            "required": [
              "id",
              "name",
              "environment",
              "status"
            ],
            "description": "Its id, name, environment (`staging`, `production`, or null when it has none) and status."
          },
          "keyId": {
            "type": "string",
            "description": "The key id you authenticated with."
          },
          "rotating": {
            "type": "boolean",
            "description": "A new secret was issued in the last 24 hours; the previous one keeps working until then."
          }
        },
        "required": [
          "connection",
          "keyId",
          "rotating"
        ],
        "description": "The connection behind your key."
      },
      "EventResponse": {
        "type": "object",
        "properties": {
          "recorded": {
            "type": "boolean"
          },
          "duplicate": {
            "type": "boolean"
          }
        },
        "required": [
          "recorded",
          "duplicate"
        ],
        "description": "`recorded: true` — saved. `duplicate: true` — an event with this `idempotencyKey` was already recorded, so this one wasn't."
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "A stable code: `invalid`, `invalid_json`, `unauthorized`, `not_found`, `api_disabled`, `body_too_large`, `rate_limited`, `internal` or `unavailable`."
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldError"
            },
            "description": "For `invalid`: each problem, by path."
          },
          "message": {
            "type": "string",
            "description": "Sometimes: a hint in words."
          }
        },
        "required": [
          "error"
        ],
        "description": "An error. `error` is a stable code; a 400 lists what was wrong in `fields`."
      },
      "FieldError": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Where, e.g. `traits.plan` or `timezone`; `(body)` for the body as a whole."
          },
          "message": {
            "type": "string",
            "description": "What's wrong."
          }
        },
        "required": [
          "path",
          "message"
        ],
        "description": "One problem with a request body."
      },
      "ContextRequest": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256
          },
          "purpose": {
            "type": "string",
            "enum": [
              "send",
              "prepare",
              "condition",
              "test"
            ]
          },
          "journeyId": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 64
              },
              {
                "type": "null"
              }
            ]
          },
          "nodeId": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 64
              },
              {
                "type": "null"
              }
            ]
          },
          "entity": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "maxLength": 128
                  },
                  "kind": {
                    "type": "string",
                    "maxLength": 40
                  }
                },
                "required": [
                  "id",
                  "kind"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "requestId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          }
        },
        "required": [
          "userId",
          "purpose",
          "requestId"
        ],
        "description": "What YouGrow POSTs to your context endpoint about one user."
      },
      "WebhookPayload": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "type": {
            "type": "string",
            "enum": [
              "email_preferences.updated",
              "email.suppressed",
              "connection.test"
            ]
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "data": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ],
        "description": "What YouGrow POSTs to your webhook endpoint."
      }
    },
    "parameters": {
      "UserId": {
        "name": "userId",
        "in": "path",
        "required": true,
        "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.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 256
        },
        "example": "user_123"
      }
    },
    "responses": {
      "Invalid": {
        "description": "The body isn't valid (or the `userId` in the URL isn't): `fields` lists each problem. Fix it; don't retry it as it is.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "invalid": {
                "summary": "Invalid fields",
                "value": {
                  "error": "invalid",
                  "fields": [
                    {
                      "path": "subscribed",
                      "message": "Invalid input: expected boolean, received string"
                    },
                    {
                      "path": "traits.email",
                      "message": "send email as a top-level field, not a trait"
                    }
                  ]
                }
              },
              "invalid_json": {
                "summary": "Not JSON",
                "value": {
                  "error": "invalid_json"
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The key id or secret is missing or wrong, or the connection was revoked.",
        "headers": {
          "WWW-Authenticate": {
            "description": "Always `Basic realm=\"YouGrow API\", charset=\"UTF-8\"`.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "unauthorized": {
                "summary": "Wrong key id or secret",
                "value": {
                  "error": "unauthorized",
                  "message": "Use HTTP Basic auth: your key id as the username, your secret as the password."
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "YouGrow doesn't hold this user (never sent, or deleted) — or API v2 isn't switched on for this YouGrow.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "not_found": {
                "summary": "Unknown user",
                "value": {
                  "error": "not_found"
                }
              },
              "api_disabled": {
                "summary": "API v2 is off here",
                "value": {
                  "error": "api_disabled",
                  "message": "API v2 isn't enabled on this YouGrow."
                }
              }
            }
          }
        }
      },
      "TooLarge": {
        "description": "The body is over 512 KB.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "body_too_large": {
                "summary": "Too large",
                "value": {
                  "error": "body_too_large",
                  "message": "At most 512 KB."
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Over the rate limit (600 requests a minute or 20,000 an hour, per key). Wait `Retry-After` seconds, then retry.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "rate_limited": {
                "summary": "Slow down",
                "value": {
                  "error": "rate_limited"
                }
              }
            }
          }
        }
      },
      "ServerError": {
        "description": "Something failed on our side. Retry with backoff (after `Retry-After`, when given) — every write is idempotent.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait, when given.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "internal": {
                "summary": "500",
                "value": {
                  "error": "internal"
                }
              },
              "unavailable": {
                "summary": "503",
                "value": {
                  "error": "unavailable"
                }
              }
            }
          }
        }
      }
    },
    "securitySchemes": {
      "basicAuth": {
        "type": "http",
        "scheme": "basic",
        "description": "Username: your key id (`ygk_…`). Password: your secret (`ygs_…`). HTTPS only. After a rotation, the previous secret works for 24 hours."
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "On YouGrow's requests to you: an ES256 JWT. Verify it with the public keys at https://yougrow.ai/.well-known/jwks.json — `iss` = `https://yougrow.ai`, `aud` = your key id, `dir` = `context` or `webhook`, `exp`, and `body_sha256` = the base64url SHA-256 of the raw body."
      }
    }
  }
}
