{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://partners.chia.health/schemas/affiliate-api-onboarding-v1.schema.json",
  "title": "Chia affiliate onboarding intake",
  "description": "Closed, invite-only input contract for chia-s2s-v1. Only `affiliate.code`, `affiliate.name`, `affiliate.contact_name`, `affiliate.contact_email` and, per asset, `destination_path` + `offer_slug` are mandatory: that minimum mints an active TEST link a partner can click immediately. Every other field is optional, carries a documented default, and may be supplied on a later run of the same intake. Deferring a field never weakens a boundary — an omitted postback template means no postback can be delivered at all, and omitted compliance evidence means no production link is issued. Completed files may contain partner postback credentials and must be transferred through an approved secure channel, never committed or emailed.",
  "type": "object",
  "additionalProperties": false,
  "required": ["schema_version", "affiliate", "assets"],
  "properties": {
    "schema_version": {
      "const": "chia-affiliate-onboarding-v1"
    },
    "affiliate": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "code",
        "name",
        "contact_name",
        "contact_email"
      ],
      "properties": {
        "code": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,32}$"
        },
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128
        },
        "contact_name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        },
        "contact_email": {
          "type": "string",
          "format": "email",
          "maxLength": 254
        },
        "realtime_channel_handle": {
          "type": "string",
          "maxLength": 255,
          "default": ""
        },
        "tracker_name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128,
          "default": "",
          "description": "Optional at onboarding. Required, together with tracker_lookback_days, repeat_postback_behaviour and value_summing_behaviour, before live postback delivery may be enabled."
        },
        "tracker_lookback_days": {
          "type": "integer",
          "minimum": 1,
          "maximum": 365,
          "description": "Optional at onboarding (stored as null). Required before live delivery may be enabled. When supplied it must leave at least two days of retry margin beyond attribution_window_days."
        },
        "repeat_postback_behaviour": {
          "enum": ["update_one_row", "create_second_row"],
          "default": "",
          "description": "Optional at onboarding. Required before live delivery may be enabled."
        },
        "value_summing_behaviour": {
          "enum": ["adds", "replaces", "not_applicable"],
          "default": "",
          "description": "Optional at onboarding. Required before live delivery may be enabled."
        },
        "attribution_window_days": {
          "type": "integer",
          "minimum": 1,
          "maximum": 30,
          "default": 30,
          "description": "Defaults to 30. When tracker_lookback_days is supplied without this field the default is lowered to min(30, tracker_lookback_days - 2) so the two-day retry margin always holds."
        },
        "suppress_zero_value": {
          "type": "boolean",
          "default": false
        }
      }
    },
    "postback": {
      "type": "object",
      "additionalProperties": false,
      "description": "Optional in full. Omitting it stores an endpoint-less configuration: no live and no test postback can be delivered, because delivery selection requires a non-empty template. Supply it on a later run once the partner's receiver exists.",
      "required": [],
      "properties": {
        "live_url_template": {
          "type": "string",
          "maxLength": 4096,
          "pattern": "^(|https://.+)$",
          "default": "",
          "description": "Empty string means 'not configured yet' and blocks all live delivery. Any non-empty value must be an https:// template and is validated in full (click_id macro in a query value, SSRF rules, distinct from the test endpoint)."
        },
        "test_url_template": {
          "type": "string",
          "maxLength": 4096,
          "pattern": "^(|https://.+)$",
          "default": "",
          "description": "Empty string means 'not configured yet'. Until it is supplied, affiliate_test_fire delivers to a mock sink and partner-receipt confirmation is refused."
        },
        "send_registration": {
          "type": "boolean",
          "default": false,
          "description": "Defaults to false: no registration event is created at all."
        },
        "vocabulary": {
          "type": "object",
          "additionalProperties": false,
          "required": ["registration", "purchase"],
          "default": {"registration": "lead", "purchase": "sale"},
          "properties": {
            "registration": {"$ref": "#/$defs/eventVocabulary"},
            "purchase": {"$ref": "#/$defs/eventVocabulary"}
          }
        },
        "failure_body_pattern": {
          "type": "string",
          "maxLength": 512,
          "default": "",
          "description": "Defaults to empty: a 200 response carrying an error body is not treated as a failure."
        },
        "timeout_seconds": {
          "const": 10,
          "default": 10,
          "description": "Fixed by the platform. The only permitted value is also the default, so this field never needs to be supplied."
        }
      }
    },
    "assets": {
      "type": "array",
      "minItems": 1,
      "maxItems": 100,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "destination_path",
          "offer_slug"
        ],
        "description": "destination_path + offer_slug mint the clickable test link. The six compliance fields are optional, but they are all-or-nothing: supply the complete set on the run that should mint the production link. Until then no production link is issued, because an issued link is append-only and could never be given its approval evidence afterwards.",
        "dependentRequired": {
          "approved_asset": ["copy_digest", "approved_by", "approved_at", "discloses_autorenewal", "discloses_material_connection"],
          "copy_digest": ["approved_asset", "approved_by", "approved_at", "discloses_autorenewal", "discloses_material_connection"],
          "approved_by": ["approved_asset", "copy_digest", "approved_at", "discloses_autorenewal", "discloses_material_connection"],
          "approved_at": ["approved_asset", "copy_digest", "approved_by", "discloses_autorenewal", "discloses_material_connection"],
          "discloses_autorenewal": ["approved_asset", "copy_digest", "approved_by", "approved_at", "discloses_material_connection"],
          "discloses_material_connection": ["approved_asset", "copy_digest", "approved_by", "approved_at", "discloses_autorenewal"]
        },
        "properties": {
          "destination_path": {
            "type": "string",
            "pattern": "^/(?!/)",
            "maxLength": 512
          },
          "offer_slug": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{1,128}$",
            "description": "Chia-internal destination/asset association only, and the key onboarding uses to recognise an already-issued link on a rerun, so it must be unique within the file. This value is never emitted in a partner postback or report; Chia mints a separate opaque outward offer id."
          },
          "approved_asset": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1024
          },
          "copy_digest": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          },
          "approved_by": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "approved_at": {
            "type": "string",
            "format": "date-time"
          },
          "discloses_autorenewal": {
            "const": true
          },
          "discloses_material_connection": {
            "const": true
          }
        }
      }
    }
  },
  "$defs": {
    "eventVocabulary": {
      "oneOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 128
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["mapped", "status"],
          "properties": {
            "mapped": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128
            },
            "status": {
              "type": "string",
              "maxLength": 128
            }
          }
        }
      ]
    }
  }
}
