{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://api.qapla.dev/courier-events/resources/schema/courier-events-v1.schema.json",
  "title": "Qapla' Courier Events API — message envelope, schema version 1.0",
  "type": "object",
  "additionalProperties": true,
  "required": [
    "schemaVersion",
    "messageId",
    "sentAt",
    "courier",
    "events"
  ],
  "properties": {
    "schemaVersion": {
      "type": "string",
      "pattern": "^1\\.[0-9]+$",
      "description": "Schema version of this message. Major.minor."
    },
    "messageId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "description": "Unique per HTTP call. Identical on a retry of the same call. A UUID is recommended but any stable identifier is accepted."
    },
    "sentAt": {
      "$ref": "#/$defs/timestamp",
      "description": "When the sender built the message."
    },
    "courier": {
      "type": "string",
      "minLength": 1,
      "maxLength": 32,
      "pattern": "^[A-Z0-9_-]+$",
      "description": "Courier code assigned by Qapla' at onboarding. Must match the bearer token."
    },
    "events": {
      "type": "array",
      "minItems": 1,
      "maxItems": 500,
      "items": {
        "$ref": "#/$defs/event"
      }
    }
  },
  "$defs": {
    "timestamp": {
      "type": "string",
      "format": "date-time",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,6})?(Z|[+-]\\d{2}:\\d{2})$",
      "description": "ISO 8601 date-time with an explicit UTC offset. The pattern is asserted because `format` is annotation-only in JSON Schema 2020-12: a naive timestamp must fail validation in the sender's own CI, not on our side."
    },
    "date": {
      "type": "string",
      "format": "date",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
    },
    "event": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "eventId",
        "occurredAt",
        "status"
      ],
      "properties": {
        "eventId": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128,
          "description": "Stable, permanent identity of this event in the courier's systems. Idempotency key."
        },
        "trackingNumber": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "description": "The tracking number the consumer knows. Either this or externalId is required."
        },
        "externalId": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128,
          "description": "Alternative identifier when the courier does not key its events off the tracking number — typically the merchant's order reference. Either trackingNumber or externalId is required; send both when you have both. When you send externalId without trackingNumber, senderAccount is required too: order references are generated by your own customers, so they are not unique across them."
        },
        "senderAccount": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "description": "The account, contract or customer code the shipment was booked under — the code that identifies the merchant on your side. Required when externalId is used without trackingNumber, because it is what makes the order reference unambiguous. Always welcome otherwise."
        },
        "occurredAt": {
          "$ref": "#/$defs/timestamp",
          "description": "When the event happened in the real world. Local time of the place where the event happened, with explicit offset; Z is accepted but loses the local reading."
        },
        "status": {
          "$ref": "#/$defs/status"
        },
        "location": {
          "$ref": "#/$defs/location"
        },
        "parcel": {
          "$ref": "#/$defs/parcel"
        },
        "reason": {
          "$ref": "#/$defs/reason"
        },
        "delivery": {
          "$ref": "#/$defs/delivery"
        },
        "pickupPoint": {
          "$ref": "#/$defs/pickupPoint"
        },
        "trackingNumberChange": {
          "$ref": "#/$defs/trackingNumberChange"
        },
        "recipientActionUrl": {
          "type": "string",
          "format": "uri",
          "maxLength": 2048,
          "description": "Courier page where the consumer can act on the delivery."
        },
        "isReturn": {
          "type": "boolean",
          "default": false,
          "description": "True if the parcel is travelling back to the merchant."
        },
        "attributes": {
          "$ref": "#/$defs/attributes"
        }
      },
      "anyOf": [
        {
          "required": [
            "trackingNumber"
          ]
        },
        {
          "required": [
            "externalId",
            "senderAccount"
          ]
        }
      ]
    },
    "status": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "code",
        "description"
      ],
      "properties": {
        "code": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "description": "The courier's own native status code, verbatim. Not a normalised value."
        },
        "description": {
          "type": "string",
          "minLength": 1,
          "maxLength": 500,
          "description": "The courier's own human-readable text."
        }
      }
    },
    "location": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "maxLength": 255
        },
        "countryCode": {
          "type": "string",
          "pattern": "^[A-Z]{2}$",
          "description": "ISO 3166-1 alpha-2."
        },
        "postalCode": {
          "type": "string",
          "maxLength": 16
        }
      }
    },
    "parcel": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "trackingNumber": {
          "type": "string",
          "maxLength": 64
        },
        "index": {
          "type": "integer",
          "minimum": 1
        },
        "total": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "reason": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "code": {
          "type": "string",
          "maxLength": 64
        },
        "description": {
          "type": "string",
          "maxLength": 500
        }
      }
    },
    "delivery": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "estimatedDeliveryDate": {
          "$ref": "#/$defs/date"
        },
        "estimatedDeliveryWindow": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "from",
            "to"
          ],
          "properties": {
            "from": {
              "$ref": "#/$defs/timestamp"
            },
            "to": {
              "$ref": "#/$defs/timestamp"
            }
          }
        },
        "deliveredTo": {
          "type": "string",
          "enum": [
            "RECIPIENT",
            "NEIGHBOUR",
            "MAILBOX",
            "PICKUP_POINT",
            "SAFE_PLACE",
            "OTHER"
          ]
        },
        "signedBy": {
          "type": "string",
          "maxLength": 255
        },
        "podUrl": {
          "type": "string",
          "format": "uri",
          "maxLength": 2048
        },
        "attemptNumber": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "pickupPoint": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "maxLength": 64
        },
        "name": {
          "type": "string",
          "maxLength": 255
        },
        "address": {
          "type": "string",
          "maxLength": 500
        },
        "postalCode": {
          "type": "string",
          "maxLength": 16
        },
        "countryCode": {
          "type": "string",
          "pattern": "^[A-Z]{2}$"
        },
        "availableUntil": {
          "$ref": "#/$defs/timestamp"
        }
      }
    },
    "trackingNumberChange": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "newTrackingNumber"
      ],
      "properties": {
        "newTrackingNumber": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64
        },
        "reason": {
          "type": "string",
          "maxLength": 255
        }
      }
    },
    "attributes": {
      "type": "object",
      "description": "Free-form flat map for courier-specific data with no dedicated field.",
      "maxProperties": 20,
      "additionalProperties": {
        "type": "string",
        "maxLength": 500
      }
    }
  }
}
