{
  "components": {
    "schemas": {
      "CatalogOut": {
        "description": "Courses and units, to paint the partner's own tree.",
        "properties": {
          "courses": {
            "additionalProperties": {
              "additionalProperties": true,
              "type": "object"
            },
            "description": "Course id -> {title, description}",
            "examples": [
              {
                "ad": {
                  "description": "Everyday AI at work",
                  "title": "AI for the workplace"
                }
              }
            ],
            "title": "Courses",
            "type": "object"
          },
          "units": {
            "description": "Every unit of every course",
            "items": {
              "$ref": "#/components/schemas/CatalogUnit"
            },
            "title": "Units",
            "type": "array"
          }
        },
        "required": [
          "courses",
          "units"
        ],
        "title": "CatalogOut",
        "type": "object"
      },
      "CatalogUnit": {
        "description": "One unit of the catalogue.",
        "properties": {
          "course": {
            "description": "Course id",
            "examples": [
              "ad"
            ],
            "title": "Course",
            "type": "string"
          },
          "id": {
            "description": "Unit id (what the lesson paths use)",
            "examples": [
              "ad01"
            ],
            "title": "Id",
            "type": "string"
          },
          "level": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Difficulty label of the unit, if it has one",
            "examples": [
              "Básico"
            ],
            "title": "Level"
          },
          "parts": {
            "description": "The kinds of material the unit has",
            "examples": [
              [
                "leccion",
                "quiz",
                "lab",
                "chuleta"
              ]
            ],
            "items": {
              "type": "string"
            },
            "title": "Parts",
            "type": "array"
          },
          "tier_required": {
            "description": "Minimum tier that opens this unit (`pro` or `premium`)",
            "examples": [
              "pro"
            ],
            "title": "Tier Required",
            "type": "string"
          },
          "title": {
            "description": "Unit title",
            "examples": [
              "Your first draft with AI"
            ],
            "title": "Title",
            "type": "string"
          }
        },
        "required": [
          "id",
          "course",
          "title",
          "level",
          "parts",
          "tier_required"
        ],
        "title": "CatalogUnit",
        "type": "object"
      },
      "CodedError": {
        "description": "The `{code, message}` form of an error detail.",
        "properties": {
          "code": {
            "description": "Stable machine-readable code",
            "examples": [
              "quota_exceeded"
            ],
            "title": "Code",
            "type": "string"
          },
          "message": {
            "description": "Human-readable text. Do not parse it: it may change and is not translated consistently",
            "examples": [
              "Todos los asientos del contrato están activos"
            ],
            "title": "Message",
            "type": "string"
          }
        },
        "required": [
          "code",
          "message"
        ],
        "title": "CodedError",
        "type": "object"
      },
      "CodedErrorOut": {
        "description": "An error whose detail carries a machine-readable code.",
        "properties": {
          "detail": {
            "$ref": "#/components/schemas/CodedError",
            "description": "Code and message of the error"
          }
        },
        "required": [
          "detail"
        ],
        "title": "CodedErrorOut",
        "type": "object"
      },
      "CredentialIssuedData": {
        "properties": {
          "credential_type": {
            "description": "Kind of credential issued.",
            "examples": [
              "course_completion"
            ],
            "title": "Credential Type",
            "type": "string"
          },
          "issued_at": {
            "description": "When it was issued (ISO 8601).",
            "examples": [
              "2026-10-21T09:14:30+00:00"
            ],
            "title": "Issued At",
            "type": "string"
          },
          "public_id": {
            "description": "Public id of the credential; anyone can verify it with `verify_url`.",
            "examples": [
              "IA-7K2M9Q"
            ],
            "title": "Public Id",
            "type": "string"
          },
          "scope": {
            "$ref": "#/components/schemas/CredentialScope",
            "description": "What the credential covers."
          },
          "verify_url": {
            "description": "Public page where the credential can be verified.",
            "examples": [
              "https://ia-academy.io/verify/?code=IA-7K2M9Q"
            ],
            "title": "Verify Url",
            "type": "string"
          }
        },
        "required": [
          "public_id",
          "credential_type",
          "scope",
          "issued_at",
          "verify_url"
        ],
        "title": "CredentialIssuedData",
        "type": "object"
      },
      "CredentialIssuedEvent": {
        "description": "A credential was issued to the learner. The holder's name is never included.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CredentialIssuedData",
            "description": "Event-specific data."
          },
          "event": {
            "const": "credential_issued",
            "description": "Event type.",
            "examples": [
              "credential_issued"
            ],
            "title": "Event",
            "type": "string"
          },
          "event_id": {
            "description": "Unique id of the event. It does NOT change between retries: use it to deduplicate.",
            "examples": [
              "evt_1f0c9a2b7d4e5c6a8b0d1e2f3a4b5c6d"
            ],
            "title": "Event Id",
            "type": "string"
          },
          "occurred_at": {
            "description": "When the event was created (ISO 8601).",
            "examples": [
              "2026-10-21T09:14:32.481000+00:00"
            ],
            "title": "Occurred At",
            "type": "string"
          },
          "partner_learner_id": {
            "description": "The `partner_learner_id` you sent when granting the seat. No email, name or any other personal data is ever included.",
            "examples": [
              "ws-12345"
            ],
            "title": "Partner Learner Id",
            "type": "string"
          }
        },
        "required": [
          "event_id",
          "occurred_at",
          "partner_learner_id",
          "event",
          "data"
        ],
        "title": "CredentialIssuedEvent",
        "type": "object"
      },
      "CredentialScope": {
        "properties": {
          "id": {
            "description": "Id of the certified item.",
            "examples": [
              "ad"
            ],
            "title": "Id",
            "type": "string"
          },
          "kind": {
            "description": "What the credential certifies (for example a course).",
            "examples": [
              "course"
            ],
            "title": "Kind",
            "type": "string"
          },
          "title": {
            "description": "Human-readable title of the certified item.",
            "examples": [
              "AI for the workplace"
            ],
            "title": "Title",
            "type": "string"
          }
        },
        "required": [
          "kind",
          "id",
          "title"
        ],
        "title": "CredentialScope",
        "type": "object"
      },
      "ErrorOut": {
        "description": "The common error body.",
        "properties": {
          "detail": {
            "description": "Human-readable explanation. Branch on the HTTP status, not on this text",
            "examples": [
              "Clave de partner invalida"
            ],
            "title": "Detail",
            "type": "string"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorOut",
        "type": "object"
      },
      "GrantIn": {
        "description": "A batch of learners to seat.",
        "properties": {
          "learners": {
            "description": "1 to 500 learners. The batch is all-or-nothing against the seat quota.",
            "items": {
              "$ref": "#/components/schemas/LearnerIn"
            },
            "maxItems": 500,
            "minItems": 1,
            "title": "Learners",
            "type": "array"
          }
        },
        "required": [
          "learners"
        ],
        "title": "GrantIn",
        "type": "object"
      },
      "GrantOut": {
        "description": "The seats that were granted or restated.",
        "properties": {
          "results": {
            "description": "One entry per learner of the batch with the seat as it stands (new seats and reactivations first, then learners that were already active).",
            "items": {
              "$ref": "#/components/schemas/LearnerOut"
            },
            "title": "Results",
            "type": "array"
          }
        },
        "required": [
          "results"
        ],
        "title": "GrantOut",
        "type": "object"
      },
      "LearnerIn": {
        "description": "One learner to seat.",
        "properties": {
          "email": {
            "anyOf": [
              {
                "maxLength": 320,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The learner's email if you know it: the seat claims itself on the first login of that address. Without it, the learner claims by signing in with the account you linked their id to. At most 320 characters.",
            "examples": [
              "alumno@empresa-cliente.es"
            ],
            "title": "Email"
          },
          "partner_learner_id": {
            "description": "The learner's id in the PARTNER's platform (your own id, ours stays ours). 1-128 characters. Repeating an id restates the same seat.",
            "examples": [
              "ws-12345"
            ],
            "maxLength": 128,
            "title": "Partner Learner Id",
            "type": "string"
          }
        },
        "required": [
          "partner_learner_id",
          "email"
        ],
        "title": "LearnerIn",
        "type": "object"
      },
      "LearnerOut": {
        "description": "The seat as it stands after the grant.",
        "properties": {
          "email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The email bound to this seat, if any (lowercased)",
            "examples": [
              "alumno@empresa-cliente.es"
            ],
            "title": "Email"
          },
          "partner_learner_id": {
            "description": "The id you sent",
            "examples": [
              "ws-12345"
            ],
            "title": "Partner Learner Id",
            "type": "string"
          },
          "reactivated": {
            "description": "True when this call revived a previously revoked seat",
            "examples": [
              false
            ],
            "title": "Reactivated",
            "type": "boolean"
          },
          "status": {
            "description": "active | revoked",
            "examples": [
              "active"
            ],
            "title": "Status",
            "type": "string"
          }
        },
        "required": [
          "partner_learner_id",
          "email",
          "status",
          "reactivated"
        ],
        "title": "LearnerOut",
        "type": "object"
      },
      "LearnerStateOut": {
        "description": "The state of one seat.",
        "properties": {
          "current_period_end": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "When the seat dies with the contract (ISO 8601)",
            "examples": [
              "2026-11-20T00:00:00Z"
            ],
            "title": "Current Period End"
          },
          "email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The email bound to the seat, if any",
            "examples": [
              "alumno@empresa-cliente.es"
            ],
            "title": "Email"
          },
          "partner_learner_id": {
            "description": "Your id for the learner",
            "examples": [
              "ws-12345"
            ],
            "title": "Partner Learner Id",
            "type": "string"
          },
          "revoked_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Why it was revoked (only on revoked seats)",
            "examples": [
              "left the company"
            ],
            "title": "Revoked Reason"
          },
          "status": {
            "description": "active | revoked | expired (the entitlement ended with the contract)",
            "examples": [
              "active"
            ],
            "title": "Status",
            "type": "string"
          }
        },
        "required": [
          "partner_learner_id",
          "email",
          "status",
          "current_period_end",
          "revoked_reason"
        ],
        "title": "LearnerStateOut",
        "type": "object"
      },
      "LearnersOut": {
        "description": "A page of seats.",
        "properties": {
          "learners": {
            "description": "The seats of this page, oldest first. Fewer than `limit` entries means there is no next page.",
            "items": {
              "$ref": "#/components/schemas/LearnerStateOut"
            },
            "title": "Learners",
            "type": "array"
          }
        },
        "required": [
          "learners"
        ],
        "title": "LearnersOut",
        "type": "object"
      },
      "ProgressMilestoneData": {
        "properties": {
          "milestone": {
            "description": "The milestone crossed, as a percentage.",
            "enum": [
              25,
              50,
              75,
              100
            ],
            "examples": [
              50
            ],
            "title": "Milestone",
            "type": "integer"
          },
          "modules_completed": {
            "description": "Modules the learner has passed, among those your contract's tier opens.",
            "examples": [
              141
            ],
            "title": "Modules Completed",
            "type": "integer"
          },
          "modules_total": {
            "description": "Modules your contract's tier opens (the 100% reference).",
            "examples": [
              275
            ],
            "title": "Modules Total",
            "type": "integer"
          },
          "percent": {
            "description": "Exact progress at the moment of the event, in percent (0-100).",
            "examples": [
              51.3
            ],
            "title": "Percent",
            "type": "number"
          }
        },
        "required": [
          "milestone",
          "percent",
          "modules_completed",
          "modules_total"
        ],
        "title": "ProgressMilestoneData",
        "type": "object"
      },
      "ProgressMilestoneEvent": {
        "description": "The learner crossed 25%, 50%, 75% or 100% of what the contract's tier opens. Each\nmilestone is emitted once per learner.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ProgressMilestoneData",
            "description": "Event-specific data."
          },
          "event": {
            "const": "progress_milestone",
            "description": "Event type.",
            "examples": [
              "progress_milestone"
            ],
            "title": "Event",
            "type": "string"
          },
          "event_id": {
            "description": "Unique id of the event. It does NOT change between retries: use it to deduplicate.",
            "examples": [
              "evt_1f0c9a2b7d4e5c6a8b0d1e2f3a4b5c6d"
            ],
            "title": "Event Id",
            "type": "string"
          },
          "occurred_at": {
            "description": "When the event was created (ISO 8601).",
            "examples": [
              "2026-10-21T09:14:32.481000+00:00"
            ],
            "title": "Occurred At",
            "type": "string"
          },
          "partner_learner_id": {
            "description": "The `partner_learner_id` you sent when granting the seat. No email, name or any other personal data is ever included.",
            "examples": [
              "ws-12345"
            ],
            "title": "Partner Learner Id",
            "type": "string"
          }
        },
        "required": [
          "event_id",
          "occurred_at",
          "partner_learner_id",
          "event",
          "data"
        ],
        "title": "ProgressMilestoneEvent",
        "type": "object"
      },
      "ReportingOut": {
        "description": "L0 aggregates: counts only, no personal data.",
        "properties": {
          "certificates": {
            "description": "Credentials issued to the partner's learners",
            "examples": [
              57
            ],
            "title": "Certificates",
            "type": "integer"
          },
          "learners_login": {
            "description": "Seats whose learner signed in at least once",
            "examples": [
              122
            ],
            "title": "Learners Login",
            "type": "integer"
          },
          "quizzes_passed": {
            "description": "Passed quizzes among the partner's seats",
            "examples": [
              943
            ],
            "title": "Quizzes Passed",
            "type": "integer"
          },
          "seats_activate_rate": {
            "description": "seats_active / seats_contracted (0 to 1, three decimals): the number that tells whether the channel is working",
            "examples": [
              0.748
            ],
            "title": "Seats Activate Rate",
            "type": "number"
          },
          "seats_active": {
            "description": "Currently live seats",
            "examples": [
              187
            ],
            "title": "Seats Active",
            "type": "integer"
          },
          "seats_contracted": {
            "description": "Seats in the contract",
            "examples": [
              250
            ],
            "title": "Seats Contracted",
            "type": "integer"
          }
        },
        "required": [
          "seats_contracted",
          "seats_active",
          "seats_activate_rate",
          "learners_login",
          "quizzes_passed",
          "certificates"
        ],
        "title": "ReportingOut",
        "type": "object"
      },
      "ReportingSettingsBody": {
        "description": "The webhook configuration: where to send and what to send.",
        "properties": {
          "events": {
            "description": "The events you want: seat_claimed, progress_milestone, credential_issued. Empty means none: nothing is sent",
            "examples": [
              [
                "seat_claimed",
                "progress_milestone"
              ]
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 16,
            "title": "Events",
            "type": "array"
          },
          "rotate_secret": {
            "default": false,
            "description": "Ask for a new signing secret (the old one stops working at once)",
            "examples": [
              false
            ],
            "title": "Rotate Secret",
            "type": "boolean"
          },
          "webhook": {
            "anyOf": [
              {
                "maxLength": 2048,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Where we POST the report. https only (http is accepted for localhost, to test). No credentials in the URL. Empty string disables the webhook",
            "examples": [
              "https://hooks.tu-empresa.es/iacademy"
            ],
            "title": "Webhook"
          }
        },
        "required": [
          "webhook",
          "events"
        ],
        "title": "ReportingSettingsBody",
        "type": "object"
      },
      "ReportingSettingsIn": {
        "description": "What the partner asks to receive: the report webhook (the L1 opt-in).",
        "properties": {
          "reporting": {
            "$ref": "#/components/schemas/ReportingSettingsBody",
            "description": "The webhook configuration: where to send the report and what to send"
          }
        },
        "required": [
          "reporting"
        ],
        "title": "ReportingSettingsIn",
        "type": "object"
      },
      "ReportingSettingsOut": {
        "description": "The answer: what is active now, and the secret (only when it is new).",
        "properties": {
          "events": {
            "description": "The events you asked for, in catalogue order",
            "examples": [
              [
                "seat_claimed",
                "progress_milestone"
              ]
            ],
            "items": {
              "type": "string"
            },
            "title": "Events",
            "type": "array"
          },
          "secret": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The signing secret. Shown ONCE: keep it, we never show it again. Null when the secret was not renewed by this call",
            "examples": [
              "Zx9Qm4-example-signing-secret"
            ],
            "title": "Secret"
          },
          "webhook": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Where the report is sent (null: disabled)",
            "examples": [
              "https://hooks.tu-empresa.es/iacademy"
            ],
            "title": "Webhook"
          }
        },
        "required": [
          "webhook",
          "events",
          "secret"
        ],
        "title": "ReportingSettingsOut",
        "type": "object"
      },
      "ReportingStatusOut": {
        "description": "The state of the delivery: how many reports went out and how the last one ended.",
        "properties": {
          "enabled": {
            "description": "Is the webhook active (a URL and at least one event)",
            "examples": [
              true
            ],
            "title": "Enabled",
            "type": "boolean"
          },
          "events": {
            "description": "The events you asked for",
            "examples": [
              [
                "seat_claimed",
                "progress_milestone"
              ]
            ],
            "items": {
              "type": "string"
            },
            "title": "Events",
            "type": "array"
          },
          "failed": {
            "description": "Reports the partner rejected",
            "examples": [
              0
            ],
            "title": "Failed",
            "type": "integer"
          },
          "last_error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Why the last one failed, if it failed",
            "examples": [
              "HTTP 400"
            ],
            "title": "Last Error"
          },
          "last_event": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The type of the last report",
            "examples": [
              "seat_claimed"
            ],
            "title": "Last Event"
          },
          "last_event_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The id of the last report (the idempotency key)",
            "examples": [
              "evt_1f0c9a2b7d4e5c6a8b0d1e2f3a4b5c6d"
            ],
            "title": "Last Event Id"
          },
          "last_sent_at": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "When the last report went out (ISO 8601)",
            "examples": [
              "2026-10-21T09:14:33.120000+00:00"
            ],
            "title": "Last Sent At"
          },
          "last_status": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "pending | sending | sent | failed | skipped",
            "examples": [
              "sent"
            ],
            "title": "Last Status"
          },
          "pending": {
            "description": "Reports waiting to be sent",
            "examples": [
              0
            ],
            "title": "Pending",
            "type": "integer"
          },
          "sent": {
            "description": "Reports sent",
            "examples": [
              12
            ],
            "title": "Sent",
            "type": "integer"
          },
          "webhook": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Where the report is sent (null: disabled)",
            "examples": [
              "https://hooks.tu-empresa.es/iacademy"
            ],
            "title": "Webhook"
          }
        },
        "required": [
          "enabled",
          "webhook",
          "events",
          "pending",
          "sent",
          "failed",
          "last_event_id",
          "last_event",
          "last_status",
          "last_sent_at",
          "last_error"
        ],
        "title": "ReportingStatusOut",
        "type": "object"
      },
      "RevokeIn": {
        "description": "Why the seat is freed.",
        "properties": {
          "reason": {
            "description": "Why the seat is freed (mandatory: it stays in the audit). 3-500 characters",
            "examples": [
              "left the company"
            ],
            "maxLength": 500,
            "minLength": 3,
            "title": "Reason",
            "type": "string"
          }
        },
        "required": [
          "reason"
        ],
        "title": "RevokeIn",
        "type": "object"
      },
      "RevokeOut": {
        "description": "The answer of the revocation: the seat is freed (or it was already free).",
        "properties": {
          "freed": {
            "description": "True when a live seat was freed by this call; false when it was not live (unknown or already revoked). Revoking twice answers the same",
            "examples": [
              true
            ],
            "title": "Freed",
            "type": "boolean"
          },
          "ok": {
            "description": "Always true when the call succeeded",
            "examples": [
              true
            ],
            "title": "Ok",
            "type": "boolean"
          }
        },
        "required": [
          "ok",
          "freed"
        ],
        "title": "RevokeOut",
        "type": "object"
      },
      "SeatClaimedData": {
        "properties": {
          "current_period_end": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "When the seat expires with the contract (ISO 8601), if known.",
            "examples": [
              "2026-11-20T00:00:00+00:00"
            ],
            "title": "Current Period End"
          },
          "seat_status": {
            "description": "Status of the seat once claimed.",
            "examples": [
              "active"
            ],
            "title": "Seat Status",
            "type": "string"
          }
        },
        "required": [
          "seat_status",
          "current_period_end"
        ],
        "title": "SeatClaimedData",
        "type": "object"
      },
      "SeatClaimedEvent": {
        "description": "The learner signed in and claimed the seat.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SeatClaimedData",
            "description": "Event-specific data."
          },
          "event": {
            "const": "seat_claimed",
            "description": "Event type.",
            "examples": [
              "seat_claimed"
            ],
            "title": "Event",
            "type": "string"
          },
          "event_id": {
            "description": "Unique id of the event. It does NOT change between retries: use it to deduplicate.",
            "examples": [
              "evt_1f0c9a2b7d4e5c6a8b0d1e2f3a4b5c6d"
            ],
            "title": "Event Id",
            "type": "string"
          },
          "occurred_at": {
            "description": "When the event was created (ISO 8601).",
            "examples": [
              "2026-10-21T09:14:32.481000+00:00"
            ],
            "title": "Occurred At",
            "type": "string"
          },
          "partner_learner_id": {
            "description": "The `partner_learner_id` you sent when granting the seat. No email, name or any other personal data is ever included.",
            "examples": [
              "ws-12345"
            ],
            "title": "Partner Learner Id",
            "type": "string"
          }
        },
        "required": [
          "event_id",
          "occurred_at",
          "partner_learner_id",
          "event",
          "data"
        ],
        "title": "SeatClaimedEvent",
        "type": "object"
      },
      "ValidationErrorOut": {
        "description": "422 body. `detail` is a list of issues for malformed requests, a string for rejected\nvalues, or a `{code, message}` object for rejected learners.",
        "properties": {
          "detail": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/ValidationIssue"
                },
                "type": "array"
              },
              {
                "$ref": "#/components/schemas/CodedError"
              },
              {
                "type": "string"
              }
            ],
            "description": "The problem(s) with the request, in one of three shapes",
            "examples": [
              "limit debe estar entre 1 y 500"
            ],
            "title": "Detail"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ValidationErrorOut",
        "type": "object"
      },
      "ValidationIssue": {
        "description": "One problem found by request validation.",
        "properties": {
          "loc": {
            "description": "Where the problem is: the part of the request, then the field path",
            "examples": [
              [
                "body",
                "learners",
                0,
                "partner_learner_id"
              ]
            ],
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "description": "What is wrong",
            "examples": [
              "Field required"
            ],
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "description": "Kind of problem",
            "examples": [
              "missing"
            ],
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationIssue",
        "type": "object"
      }
    },
    "securitySchemes": {
      "PartnerApiKey": {
        "description": "API key of the calling partner, given once when the contract is signed. Send it in the `X-Api-Key` header of every request, reads included.",
        "in": "header",
        "name": "X-Api-Key",
        "type": "apiKey"
      }
    }
  },
  "info": {
    "description": "The IAcademy partner API lets a distribution partner sell IAcademy seats to its own client\ncompanies, entirely by machine-to-machine calls: you create seats for your learners, they sign\nin to IAcademy with their own account and learn, and you read aggregate reporting (and, if you\nopt in, per-learner events through a signed webhook).\n\nBase URL: `https://api.ia-academy.io`. All routes live under `/api/partner/v1`.\n\n# How the channel works\n\nA contract gives you a **seat quota** (`max_active_seats`) and one **access tier** (`pro` or\n`premium`, never mixed). Each seat is one learner.\n\n1. You grant seats with `POST /learners`.\n2. Each learner signs in to IAcademy with their own account and **claims** the seat (see below).\n3. You follow progress with `GET /reporting` and, optionally, the report webhook.\n4. When someone leaves, you free the seat with `DELETE /learners/{id}`. The slot can go to a\n   different learner. There are no refunds: freeing a seat is the only movement.\n\nSeats are tied to the monthly contract cycle. If the contract is not renewed, its seats expire\nwith it (status `expired`, `current_period_end` tells you when).\n\n# Authentication\n\nEvery request must carry the header `X-Api-Key: <your key>`, reads included.\n\n```bash\ncurl -sS \"https://api.ia-academy.io/api/partner/v1/reporting\" -H \"X-Api-Key: <your-key>\"\n```\n\n- The key is given **once**, when the contract is signed. IAcademy stores only its hash, so a\n  lost key cannot be recovered: a new one is issued and the old one stops working.\n- A **suspended** partner's key is treated exactly like an unknown key: both answer `401`\n  with the same body, so partner ids cannot be enumerated.\n- Keep the key on your servers. Never ship it to a browser or a mobile app.\n\n# Seats and quota\n\n- The quota is **hard**. `POST /learners` needs room for every *new or reactivated* seat of the\n  batch; if it does not fit, **nothing is granted** and the call answers `409` with code\n  `quota_exceeded`. There are no partial grants.\n- `POST /learners` is **idempotent**. Sending the same `partner_learner_id` again restates the\n  seat without using more quota; sending a revoked one reactivates it (`reactivated: true`).\n  It is safe to retry a batch after a network error.\n- A batch holds 1 to 500 learners. Your own id (`partner_learner_id`) is the only key we use\n  for the learner towards you; IAcademy's internal ids are never exposed.\n- To check how many seats are in use, count the `active` ones from `GET /learners`.\n\n# How the learner claims the seat\n\nThe learner never receives a purchase code.\n\n1. You grant the seat (and, if you know it, the learner's `email`).\n2. The learner signs in to IAcademy with their own account (OIDC login). If you gave their email,\n   the seat is matched on that first login.\n3. On sign-in the IAcademy app calls `POST /api/auth/claim-seat` with the **learner's** session\n   (not your key; you never call it). This turns the session into course access, and it is\n   idempotent.\n4. If the seat was revoked or the contract expired, the claim is refused with `409`: that is\n   decided by you, not by the learner.\n\nWhen the seat is claimed, the `seat_claimed` webhook event fires (if you subscribed).\n\n# Data levels\n\n| Level | What | How you get it |\n|---|---|---|\n| **L0** (default) | Aggregates of the contract: seats, activation rate, logins, quizzes passed, credentials. No personal data. | `GET /reporting` |\n| **L1** (opt-in) | Per-learner events, identified **only** by your own `partner_learner_id`. | Report webhook, enabled with `PATCH /settings` |\n\nNothing at level L1 is sent until you opt in. Under no level do we send a learner's real name,\nIP address, question history or tutor conversations. The webhook payload carries no email or\nname either.\n\n# Errors\n\nEvery error is JSON with a `detail` key, in one of two forms:\n\n```json\n{\"detail\": \"Human-readable text\"}\n```\n\n```json\n{\"detail\": {\"code\": \"quota_exceeded\", \"message\": \"Human-readable text\"}}\n```\n\nRequest validation errors (`422`) use a list of issues instead:\n`{\"detail\": [{\"loc\": [...], \"msg\": \"...\", \"type\": \"...\"}]}`.\n\nBranch on the HTTP status and, when present, on `code`; never on the text of `message` or a\nstring `detail`.\n\n| Status | Meaning |\n|---|---|\n| `401` | `X-Api-Key` missing, unknown or suspended. |\n| `404` | The learner does not exist in your contract. |\n| `409` | The batch does not fit the seat quota (`quota_exceeded`). |\n| `422` | Invalid input. |\n| `429` | Rate limit: 120 requests per minute per partner on every route except the catalogue. |\n\n# Pagination\n\n`GET /learners` pages with `limit` (1 to 500, default 100) and `offset` (0 or more), oldest seat\nfirst. Request pages until one returns fewer than `limit` seats.\n\n# Report webhook (signed)\n\nIf you opt in with `PATCH /settings`, IAcademy POSTs JSON events to your HTTPS URL when\nsomething happens, instead of you having to ask.\n\n**Events** you can subscribe to:\n\n| Event | When |\n|---|---|\n| `seat_claimed` | A learner claimed their seat. |\n| `progress_milestone` | A learner crossed 25%, 50%, 75% or 100% of the units your tier opens. Once per milestone. |\n| `credential_issued` | A credential was issued to a learner. |\n\nTheir exact payloads are in the **Webhook events** section of this reference. Every body has\nthis envelope:\n\n```json\n{\n  \"event_id\": \"evt_1f0c9a2b7d4e5c6a8b0d1e2f3a4b5c6d\",\n  \"event\": \"progress_milestone\",\n  \"occurred_at\": \"2026-10-21T09:14:32.481000+00:00\",\n  \"partner_learner_id\": \"ws-12345\",\n  \"data\": {\"milestone\": 50, \"percent\": 51.3, \"modules_completed\": 141, \"modules_total\": 275}\n}\n```\n\n**Signature.** Each delivery carries `X-IA-Signature`: the lowercase hex **HMAC-SHA256 of the\nexact body bytes**, keyed with the signing secret. The secret is generated by IAcademy and\nreturned **once** by `PATCH /settings` (ask for another with `rotate_secret: true`; the old one\nstops working at once). Verify over the raw bytes you received, not over re-serialized JSON, and\ncompare in constant time:\n\n```python\nimport hashlib, hmac\n\ndef is_valid(secret: str, body: bytes, header: str) -> bool:\n    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, header)\n```\n\nTwo more headers help routing without parsing the body: `X-IA-Event` (the event type) and\n`X-IA-Event-Id` (same as `event_id`).\n\n**Delivery and retries.**\n\n- Answer any `2xx`: the event is delivered and not sent again.\n- `429` or `5xx`, or an unreachable URL: retried later with growing delays, a limited number of\n  times. The `event_id` does **not** change between retries.\n- Any other `4xx`: not retried; the event is recorded as failed.\n\nStore the `event_id` before processing and ignore ids you have already seen: that is your\nidempotency key. `GET /reporting/status` shows what is pending, sent and failed.\n\nThe URL must be `https` (plain `http` is accepted only for `localhost`, to test) and cannot\ncontain credentials. To turn the webhook off, send an empty `webhook` or an empty `events` list.\n\n# Versioning\n\nThe API is versioned in the path (`/api/partner/v1`). Within a version we only\n**add** fields, and unknown fields in your requests are ignored, so your client must tolerate\nfields it does not know. A breaking change is published as a new route, never as a change to an\nexisting one.\n",
    "title": "IAcademy partner API",
    "version": "1.0.0"
  },
  "openapi": "3.1.0",
  "paths": {
    "/api/partner/v1/catalog": {
      "get": {
        "description": "Every course and unit, with the `tier_required` that gates it inside IAcademy, so you\ncan paint your own tree: compare `tier_required` with the tier of your contract.\n\nNo media URLs and no lesson content: those live behind the learner's session on the\nIAcademy platform.",
        "operationId": "getCatalog",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "invalid": {
                    "summary": "Unknown or suspended key",
                    "value": {
                      "detail": "Clave de partner invalida"
                    }
                  },
                  "missing": {
                    "summary": "No header",
                    "value": {
                      "detail": "Falta la clave de partner"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "The `X-Api-Key` header is missing, unknown, or belongs to a suspended partner. Unknown and suspended keys answer identically, so partners cannot be enumerated."
          }
        },
        "security": [
          {
            "PartnerApiKey": []
          }
        ],
        "summary": "Get the catalogue with tier gates",
        "tags": [
          "Catalogue"
        ]
      }
    },
    "/api/partner/v1/learners": {
      "get": {
        "description": "Seats of this contract, oldest first, including revoked and expired ones.\n\nPage with `limit` (1-500, default 100) and `offset`: keep adding `limit` to `offset` until\na page comes back with fewer than `limit` seats. To know how many seats are in use, count\nthe `active` ones.",
        "operationId": "listLearners",
        "parameters": [
          {
            "description": "Page size, 1 to 500.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "description": "Page size, 1 to 500.",
              "examples": [
                100
              ],
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "description": "Seats to skip, 0 or more.",
            "in": "query",
            "name": "offset",
            "required": false,
            "schema": {
              "default": 0,
              "description": "Seats to skip, 0 or more.",
              "examples": [
                0
              ],
              "title": "Offset",
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LearnersOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "invalid": {
                    "summary": "Unknown or suspended key",
                    "value": {
                      "detail": "Clave de partner invalida"
                    }
                  },
                  "missing": {
                    "summary": "No header",
                    "value": {
                      "detail": "Falta la clave de partner"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "The `X-Api-Key` header is missing, unknown, or belongs to a suspended partner. Unknown and suspended keys answer identically, so partners cannot be enumerated."
          },
          "422": {
            "content": {
              "application/json": {
                "examples": {
                  "limit": {
                    "summary": "Bad limit",
                    "value": {
                      "detail": "limit debe estar entre 1 y 500"
                    }
                  },
                  "offset": {
                    "summary": "Bad offset",
                    "value": {
                      "detail": "offset no puede ser negativo"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorOut"
                }
              }
            },
            "description": "`limit` or `offset` out of range."
          },
          "429": {
            "content": {
              "application/json": {
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "detail": "Demasiadas peticiones; espera un minuto"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "Rate limit: 120 requests per minute per partner, on every route. Wait and retry."
          }
        },
        "security": [
          {
            "PartnerApiKey": []
          }
        ],
        "summary": "List seats",
        "tags": [
          "Seats"
        ]
      },
      "post": {
        "description": "Grant 1 to 500 seats in one call.\n\n**Idempotent**: repeating the same `partner_learner_id` restates its seat without using\nmore quota; granting a revoked one reactivates it (`reactivated: true`).\n\n**Hard quota**: the seats a batch needs (new ones plus reactivations) must fit in the\ncontract's `max_active_seats`. If they do not, nothing is granted and the call answers\n`409` with code `quota_exceeded`. Free seats with `DELETE /learners/{id}` and retry.\n\nThe learner then claims the seat by signing in; see the claim flow in the introduction.",
        "operationId": "grantLearners",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GrantIn"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GrantOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "invalid": {
                    "summary": "Unknown or suspended key",
                    "value": {
                      "detail": "Clave de partner invalida"
                    }
                  },
                  "missing": {
                    "summary": "No header",
                    "value": {
                      "detail": "Falta la clave de partner"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "The `X-Api-Key` header is missing, unknown, or belongs to a suspended partner. Unknown and suspended keys answer identically, so partners cannot be enumerated."
          },
          "409": {
            "content": {
              "application/json": {
                "examples": {
                  "quota": {
                    "summary": "Quota exceeded",
                    "value": {
                      "detail": {
                        "code": "quota_exceeded",
                        "message": "Todos los asientos del contrato están activos"
                      }
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CodedErrorOut"
                }
              }
            },
            "description": "The batch does not fit in the seat quota: nothing was granted (all-or-nothing)."
          },
          "422": {
            "content": {
              "application/json": {
                "examples": {
                  "learner": {
                    "summary": "Rejected learner id",
                    "value": {
                      "detail": {
                        "code": "invalid_learner_id",
                        "message": "partner_learner_id es obligatorio y de max 128 caracteres"
                      }
                    }
                  },
                  "validation": {
                    "summary": "Missing field",
                    "value": {
                      "detail": [
                        {
                          "loc": [
                            "body",
                            "learners",
                            0,
                            "partner_learner_id"
                          ],
                          "msg": "Field required",
                          "type": "missing"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorOut"
                }
              }
            },
            "description": "Malformed body (empty or over 500 learners, wrong types) or a rejected learner."
          },
          "429": {
            "content": {
              "application/json": {
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "detail": "Demasiadas peticiones; espera un minuto"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "Rate limit: 120 requests per minute per partner, on every route. Wait and retry."
          }
        },
        "security": [
          {
            "PartnerApiKey": []
          }
        ],
        "summary": "Grant seats in bulk",
        "tags": [
          "Seats"
        ]
      }
    },
    "/api/partner/v1/learners/{partner_learner_id}": {
      "delete": {
        "description": "Free a seat (someone left the company, an order correction). The `reason` is mandatory\nand kept in the audit.\n\nThere are **no refunds**: freeing the seat is the only movement, and it reopens the slot\nfor a different learner. Idempotent: revoking an already revoked or unknown seat answers\n`200` with `freed: false`.",
        "operationId": "revokeLearner",
        "parameters": [
          {
            "description": "Your id for the learner, as sent when granting the seat.",
            "in": "path",
            "name": "partner_learner_id",
            "required": true,
            "schema": {
              "description": "Your id for the learner, as sent when granting the seat.",
              "examples": [
                "ws-12345"
              ],
              "title": "Partner Learner Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RevokeIn"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RevokeOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "invalid": {
                    "summary": "Unknown or suspended key",
                    "value": {
                      "detail": "Clave de partner invalida"
                    }
                  },
                  "missing": {
                    "summary": "No header",
                    "value": {
                      "detail": "Falta la clave de partner"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "The `X-Api-Key` header is missing, unknown, or belongs to a suspended partner. Unknown and suspended keys answer identically, so partners cannot be enumerated."
          },
          "422": {
            "content": {
              "application/json": {
                "examples": {
                  "reason": {
                    "summary": "Reason too short",
                    "value": {
                      "detail": [
                        {
                          "loc": [
                            "body",
                            "reason"
                          ],
                          "msg": "String should have at least 3 characters",
                          "type": "string_too_short"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorOut"
                }
              }
            },
            "description": "Missing or out-of-range `reason` (3-500 characters)."
          },
          "429": {
            "content": {
              "application/json": {
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "detail": "Demasiadas peticiones; espera un minuto"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "Rate limit: 120 requests per minute per partner, on every route. Wait and retry."
          }
        },
        "security": [
          {
            "PartnerApiKey": []
          }
        ],
        "summary": "Free a seat",
        "tags": [
          "Seats"
        ]
      },
      "get": {
        "description": "The state of one seat: `active`, `revoked` (with `revoked_reason`) or `expired` (the\ncontract ended). `404` when the id never existed in your contract: state is never guessed.",
        "operationId": "getLearner",
        "parameters": [
          {
            "description": "Your id for the learner, as sent when granting the seat.",
            "in": "path",
            "name": "partner_learner_id",
            "required": true,
            "schema": {
              "description": "Your id for the learner, as sent when granting the seat.",
              "examples": [
                "ws-12345"
              ],
              "title": "Partner Learner Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LearnerStateOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "invalid": {
                    "summary": "Unknown or suspended key",
                    "value": {
                      "detail": "Clave de partner invalida"
                    }
                  },
                  "missing": {
                    "summary": "No header",
                    "value": {
                      "detail": "Falta la clave de partner"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "The `X-Api-Key` header is missing, unknown, or belongs to a suspended partner. Unknown and suspended keys answer identically, so partners cannot be enumerated."
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "unknown": {
                    "summary": "Unknown learner",
                    "value": {
                      "detail": "Ese alumno no existe en el contrato"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "The seat never existed in this contract."
          },
          "422": {
            "content": {
              "application/json": {
                "examples": {
                  "issue": {
                    "summary": "Validation issue",
                    "value": {
                      "detail": [
                        {
                          "loc": [
                            "path",
                            "partner_learner_id"
                          ],
                          "msg": "Field required",
                          "type": "missing"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorOut"
                }
              }
            },
            "description": "Declared for client generators: the path id is any string, so in practice this route answers 404 for a bad id, not 422."
          },
          "429": {
            "content": {
              "application/json": {
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "detail": "Demasiadas peticiones; espera un minuto"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "Rate limit: 120 requests per minute per partner, on every route. Wait and retry."
          }
        },
        "security": [
          {
            "PartnerApiKey": []
          }
        ],
        "summary": "Get one seat",
        "tags": [
          "Seats"
        ]
      }
    },
    "/api/partner/v1/reporting": {
      "get": {
        "description": "Aggregates of the contract (data level L0): counts only, no personal data of any\nlearner. `seats_activate_rate` is the one number that tells whether the channel is working.\n\nPer-learner data (level L1) is never returned here: it is only sent, on your opt-in, by the\nreport webhook (see `PATCH /settings`).",
        "operationId": "getReporting",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportingOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "invalid": {
                    "summary": "Unknown or suspended key",
                    "value": {
                      "detail": "Clave de partner invalida"
                    }
                  },
                  "missing": {
                    "summary": "No header",
                    "value": {
                      "detail": "Falta la clave de partner"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "The `X-Api-Key` header is missing, unknown, or belongs to a suspended partner. Unknown and suspended keys answer identically, so partners cannot be enumerated."
          },
          "429": {
            "content": {
              "application/json": {
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "detail": "Demasiadas peticiones; espera un minuto"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "Rate limit: 120 requests per minute per partner, on every route. Wait and retry."
          }
        },
        "security": [
          {
            "PartnerApiKey": []
          }
        ],
        "summary": "Get L0 aggregate reporting",
        "tags": [
          "Reporting"
        ]
      }
    },
    "/api/partner/v1/reporting/status": {
      "get": {
        "description": "How the report deliveries are going: how many went out, how many are waiting and how\nthe last one ended. Answers 200 even if you never turned the webhook on (then everything is\nzero and `enabled` is false): asking is not an error.",
        "operationId": "getReportingStatus",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportingStatusOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "invalid": {
                    "summary": "Unknown or suspended key",
                    "value": {
                      "detail": "Clave de partner invalida"
                    }
                  },
                  "missing": {
                    "summary": "No header",
                    "value": {
                      "detail": "Falta la clave de partner"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "The `X-Api-Key` header is missing, unknown, or belongs to a suspended partner. Unknown and suspended keys answer identically, so partners cannot be enumerated."
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "unknown": {
                    "summary": "Partner not found",
                    "value": {
                      "detail": "Partner no encontrado"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "The partner record could not be found (should not happen for a valid key; contact support)."
          },
          "429": {
            "content": {
              "application/json": {
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "detail": "Demasiadas peticiones; espera un minuto"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "Rate limit: 120 requests per minute per partner, on every route. Wait and retry."
          }
        },
        "security": [
          {
            "PartnerApiKey": []
          }
        ],
        "summary": "Get the webhook delivery status",
        "tags": [
          "Webhook"
        ]
      }
    },
    "/api/partner/v1/settings": {
      "patch": {
        "description": "Turn the report webhook on (or off). This is the opt-in of the L1: until you call this,\nnothing is sent to you and `GET /reporting` keeps answering with the L0 aggregates only.\nThe events, payloads and signature are documented in the webhook section of this reference.\n\nSend your receiving URL and the events you want. The signing secret is OURS: we generate it\nand return it once, here. Keep it: the report is signed with it, and we never show it again\n(ask for a new one with `rotate_secret` if you lose it).\n\nTo turn it off: send an empty `webhook` (or an empty `events` list) — nothing goes out from\nthen on, and nothing that went out comes back.",
        "operationId": "updateSettings",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportingSettingsIn"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportingSettingsOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "invalid": {
                    "summary": "Unknown or suspended key",
                    "value": {
                      "detail": "Clave de partner invalida"
                    }
                  },
                  "missing": {
                    "summary": "No header",
                    "value": {
                      "detail": "Falta la clave de partner"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "The `X-Api-Key` header is missing, unknown, or belongs to a suspended partner. Unknown and suspended keys answer identically, so partners cannot be enumerated."
          },
          "422": {
            "content": {
              "application/json": {
                "examples": {
                  "event": {
                    "summary": "Unknown event",
                    "value": {
                      "detail": "Eventos desconocidos: foo"
                    }
                  },
                  "url": {
                    "summary": "Not https",
                    "value": {
                      "detail": "La URL del webhook debe usar https"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorOut"
                }
              }
            },
            "description": "Invalid body, a webhook URL that is not https (or has credentials), or an unknown event type."
          },
          "429": {
            "content": {
              "application/json": {
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "detail": "Demasiadas peticiones; espera un minuto"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorOut"
                }
              }
            },
            "description": "Rate limit: 120 requests per minute per partner, on every route. Wait and retry."
          }
        },
        "security": [
          {
            "PartnerApiKey": []
          }
        ],
        "summary": "Configure the report webhook",
        "tags": [
          "Webhook"
        ]
      }
    }
  },
  "servers": [
    {
      "description": "Production",
      "url": "https://api.ia-academy.io"
    }
  ],
  "tags": [
    {
      "description": "Grant, inspect and free the seats of your contract.",
      "name": "Seats"
    },
    {
      "description": "Courses and units, with the tier that opens each.",
      "name": "Catalogue"
    },
    {
      "description": "L0 aggregate reporting: counts, no personal data.",
      "name": "Reporting"
    },
    {
      "description": "Opt in to the signed report webhook and follow its deliveries.",
      "name": "Webhook"
    },
    {
      "description": "What IAcademy POSTs to your URL. Not routes you call: routes you serve.",
      "name": "Webhook events"
    }
  ],
  "webhooks": {
    "credential_issued": {
      "post": {
        "description": "Sent when a credential is issued to the learner. It carries the public id and the\nverification URL, never the holder's name.",
        "operationId": "webhookCredentialIssued",
        "parameters": [
          {
            "description": "Lowercase hex HMAC-SHA256 of the exact request body bytes, keyed with the signing secret returned by `PATCH /settings`. Verify it before trusting the body.",
            "in": "header",
            "name": "x-ia-signature",
            "required": true,
            "schema": {
              "description": "Lowercase hex HMAC-SHA256 of the exact request body bytes, keyed with the signing secret returned by `PATCH /settings`. Verify it before trusting the body.",
              "examples": [
                "3f7a9c1e5b0d4a6f8e2c1b3d5a7f9e0c2b4d6f8a1c3e5b7d9f0a2c4e6b8d0f1a"
              ],
              "title": "X-Ia-Signature",
              "type": "string"
            }
          },
          {
            "description": "The event type, same as `event` in the body.",
            "in": "header",
            "name": "x-ia-event",
            "required": true,
            "schema": {
              "description": "The event type, same as `event` in the body.",
              "examples": [
                "credential_issued"
              ],
              "title": "X-Ia-Event",
              "type": "string"
            }
          },
          {
            "description": "The `event_id` of the body.",
            "in": "header",
            "name": "x-ia-event-id",
            "required": true,
            "schema": {
              "description": "The `event_id` of the body.",
              "examples": [
                "evt_1f0c9a2b7d4e5c6a8b0d1e2f3a4b5c6d"
              ],
              "title": "X-Ia-Event-Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CredentialIssuedEvent"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "2XX": {
            "description": "Delivered. The event is considered received and is not sent again."
          },
          "429": {
            "description": "Transient: the delivery is retried later, same `event_id`."
          },
          "4XX": {
            "description": "Rejected: NOT retried. The event is recorded as failed."
          },
          "5XX": {
            "description": "Transient: the delivery is retried later, same `event_id`."
          }
        },
        "summary": "credential_issued",
        "tags": [
          "Webhook events"
        ]
      }
    },
    "progress_milestone": {
      "post": {
        "description": "Sent when the learner crosses 25%, 50%, 75% or 100% of the units your contract's tier\nopens (not of the whole catalogue). Each milestone is sent once per learner.",
        "operationId": "webhookProgressMilestone",
        "parameters": [
          {
            "description": "Lowercase hex HMAC-SHA256 of the exact request body bytes, keyed with the signing secret returned by `PATCH /settings`. Verify it before trusting the body.",
            "in": "header",
            "name": "x-ia-signature",
            "required": true,
            "schema": {
              "description": "Lowercase hex HMAC-SHA256 of the exact request body bytes, keyed with the signing secret returned by `PATCH /settings`. Verify it before trusting the body.",
              "examples": [
                "3f7a9c1e5b0d4a6f8e2c1b3d5a7f9e0c2b4d6f8a1c3e5b7d9f0a2c4e6b8d0f1a"
              ],
              "title": "X-Ia-Signature",
              "type": "string"
            }
          },
          {
            "description": "The event type, same as `event` in the body.",
            "in": "header",
            "name": "x-ia-event",
            "required": true,
            "schema": {
              "description": "The event type, same as `event` in the body.",
              "examples": [
                "progress_milestone"
              ],
              "title": "X-Ia-Event",
              "type": "string"
            }
          },
          {
            "description": "The `event_id` of the body.",
            "in": "header",
            "name": "x-ia-event-id",
            "required": true,
            "schema": {
              "description": "The `event_id` of the body.",
              "examples": [
                "evt_1f0c9a2b7d4e5c6a8b0d1e2f3a4b5c6d"
              ],
              "title": "X-Ia-Event-Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProgressMilestoneEvent"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "2XX": {
            "description": "Delivered. The event is considered received and is not sent again."
          },
          "429": {
            "description": "Transient: the delivery is retried later, same `event_id`."
          },
          "4XX": {
            "description": "Rejected: NOT retried. The event is recorded as failed."
          },
          "5XX": {
            "description": "Transient: the delivery is retried later, same `event_id`."
          }
        },
        "summary": "progress_milestone",
        "tags": [
          "Webhook events"
        ]
      }
    },
    "seat_claimed": {
      "post": {
        "description": "Sent when the learner signs in and claims the seat. POSTed as JSON to the URL you set\nwith `PATCH /settings`, only if you subscribed to `seat_claimed`.",
        "operationId": "webhookSeatClaimed",
        "parameters": [
          {
            "description": "Lowercase hex HMAC-SHA256 of the exact request body bytes, keyed with the signing secret returned by `PATCH /settings`. Verify it before trusting the body.",
            "in": "header",
            "name": "x-ia-signature",
            "required": true,
            "schema": {
              "description": "Lowercase hex HMAC-SHA256 of the exact request body bytes, keyed with the signing secret returned by `PATCH /settings`. Verify it before trusting the body.",
              "examples": [
                "3f7a9c1e5b0d4a6f8e2c1b3d5a7f9e0c2b4d6f8a1c3e5b7d9f0a2c4e6b8d0f1a"
              ],
              "title": "X-Ia-Signature",
              "type": "string"
            }
          },
          {
            "description": "The event type, same as `event` in the body.",
            "in": "header",
            "name": "x-ia-event",
            "required": true,
            "schema": {
              "description": "The event type, same as `event` in the body.",
              "examples": [
                "seat_claimed"
              ],
              "title": "X-Ia-Event",
              "type": "string"
            }
          },
          {
            "description": "The `event_id` of the body.",
            "in": "header",
            "name": "x-ia-event-id",
            "required": true,
            "schema": {
              "description": "The `event_id` of the body.",
              "examples": [
                "evt_1f0c9a2b7d4e5c6a8b0d1e2f3a4b5c6d"
              ],
              "title": "X-Ia-Event-Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeatClaimedEvent"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "2XX": {
            "description": "Delivered. The event is considered received and is not sent again."
          },
          "429": {
            "description": "Transient: the delivery is retried later, same `event_id`."
          },
          "4XX": {
            "description": "Rejected: NOT retried. The event is recorded as failed."
          },
          "5XX": {
            "description": "Transient: the delivery is retried later, same `event_id`."
          }
        },
        "summary": "seat_claimed",
        "tags": [
          "Webhook events"
        ]
      }
    }
  }
}
