openapi: 3.1.0
info:
  title: Binnacle AI Public API
  description: |
    Read-only REST API + outbound webhooks for Binnacle AI, the maritime
    crew compliance SaaS. Customers use this to pull fleet, crew, voyage,
    incident, insurance, and compliance data into Zapier, HubSpot, Slack,
    or their own internal tooling, and to receive HMAC-signed event
    notifications when compliance state changes.

    All endpoints are scoped to the API key's organization — there is no
    way to query records in another tenant.
  version: "1.0.0"
  contact:
    name: Binnacle AI Developer Support
    email: api@binnacleai.com
    url: https://binnacleai.com/api-docs
  license:
    name: Proprietary
    url: https://binnacleai.com/terms

servers:
  - url: https://binnacleai.com
    description: Production
  - url: http://localhost:3000
    description: Local dev

security:
  - BearerAuth: []

tags:
  - name: Vessels
    description: Fleet — vessels owned/operated by the org.
  - name: Crew
    description: Mariners + their credentials.
  - name: Credentials
    description: Licenses, STCW endorsements, TWIC, medical, drug-test, etc.
  - name: Voyages
    description: Trip records with manifests + fuel rollups.
  - name: Incidents
    description: Cyber + maritime incidents.
  - name: Insurance
    description: Policies (P&I, H&M, etc.).
  - name: Cyber
    description: 46 CFR 101.650 cyber plan compliance.
  - name: Compliance
    description: Fleet-wide compliance scoring rollups.

paths:
  /api/v1/vessels:
    get:
      tags: [Vessels]
      summary: List vessels
      operationId: listVessels
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Paginated list of vessels.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Vessel" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /api/v1/vessels/{id}:
    get:
      tags: [Vessels]
      summary: Get a single vessel
      operationId: getVessel
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Single vessel with crew + COI requirement detail.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/VesselDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/crew:
    get:
      tags: [Crew]
      summary: List crew
      operationId: listCrew
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Paginated crew list with nested credentials.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/CrewMember" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v1/crew/{id}:
    get:
      tags: [Crew]
      summary: Get a single crew member
      operationId: getCrewMember
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Single crew member with full credential + assignment detail.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CrewMemberDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/credentials/expiring:
    get:
      tags: [Credentials]
      summary: List credentials expiring within a window
      operationId: listExpiringCredentials
      parameters:
        - in: query
          name: days
          schema:
            type: integer
            default: 30
            minimum: 1
            maximum: 365
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Credentials expiring within `days` days (includes already-expired).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/CredentialExpiring" }
                      query:
                        type: object
                        properties:
                          days: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v1/voyages:
    get:
      tags: [Voyages]
      summary: List voyages
      operationId: listVoyages
      parameters:
        - in: query
          name: status
          schema:
            type: string
            enum: [PLANNED, UNDERWAY, COMPLETED, CANCELLED]
        - in: query
          name: vessel_id
          schema: { type: string }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Paginated voyages with vessel + manifest summary.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Voyage" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v1/incidents:
    get:
      tags: [Incidents]
      summary: List cyber incidents
      operationId: listIncidents
      parameters:
        - in: query
          name: severity
          schema: { type: string, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        - in: query
          name: status
          schema:
            type: string
            enum: [REPORTED, INVESTIGATING, CONTAINED, CLOSED, REPORTED_TO_NRC]
        - in: query
          name: vessel_id
          schema: { type: string }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Paginated incidents.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Incident" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v1/insurance/policies:
    get:
      tags: [Insurance]
      summary: List insurance policies
      operationId: listInsurancePolicies
      parameters:
        - in: query
          name: status
          schema: { type: string }
        - in: query
          name: type
          schema: { type: string }
        - in: query
          name: vessel_id
          schema: { type: string }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Paginated insurance policies (P&I, H&M, etc.).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/InsurancePolicy" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v1/cyber/plan:
    get:
      tags: [Cyber]
      summary: Get the org's cyber plan(s)
      operationId: getCyberPlan
      responses:
        "200":
          description: Cyber plans with element rollup.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CyberPlan" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v1/compliance/summary:
    get:
      tags: [Compliance]
      summary: Fleet-wide compliance summary
      operationId: getComplianceSummary
      responses:
        "200":
          description: Overall score, grade, vessel/crew/credential rollups.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ComplianceSummary" }
        "401": { $ref: "#/components/responses/Unauthorized" }

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: bnk_live_<32 chars>
      description: |
        Issue a key from `Admin → API keys` in the dashboard. Pass it as
        `Authorization: Bearer bnk_live_<key>` on every request. Tokens
        are bcrypt-hashed at rest; the cleartext is shown once on creation.

  parameters:
    Limit:
      in: query
      name: limit
      schema:
        type: integer
        default: 50
        minimum: 1
        maximum: 200
    Offset:
      in: query
      name: offset
      schema:
        type: integer
        default: 0
        minimum: 0

  headers:
    XRateLimitLimit:
      description: Sustained per-minute request cap for this key.
      schema: { type: integer }
    XRateLimitRemaining:
      description: Requests remaining in the current 60s window.
      schema: { type: integer }
    XRateLimitReset:
      description: Unix epoch seconds when the window resets.
      schema: { type: integer }

  responses:
    Unauthorized:
      description: Missing, malformed, revoked, or expired API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Record not found in this org.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Too many requests.
      headers:
        Retry-After:
          schema: { type: integer }
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/XRateLimitReset"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          example: InvalidApiKey
        message:
          type: string
          example: "Provide Authorization: Bearer ..."
        retryAfter:
          type: integer
          description: Seconds — only set on 429.

    PaginatedEnvelope:
      type: object
      required: [data, pagination, links]
      properties:
        data:
          type: array
          items: {}
        pagination:
          type: object
          required: [limit, offset, total]
          properties:
            limit: { type: integer }
            offset: { type: integer }
            total: { type: integer }
        links:
          type: object
          required: [self]
          properties:
            self: { type: string, format: uri }
            next:
              oneOf:
                - { type: string, format: uri }
                - { type: "null" }
            prev:
              oneOf:
                - { type: string, format: uri }
                - { type: "null" }

    Vessel:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        hin:
          oneOf: [{ type: string }, { type: "null" }]
        official_number:
          oneOf: [{ type: string }, { type: "null" }]
        gross_tonnage:
          oneOf: [{ type: number }, { type: "null" }]
        subchapter:
          type: string
          enum: [K, H, T, L, I, OTHER]
        home_port:
          oneOf: [{ type: string }, { type: "null" }]
        coi_expiration:
          oneOf: [{ type: string, format: date-time }, { type: "null" }]
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    VesselDetail:
      allOf:
        - $ref: "#/components/schemas/Vessel"
        - type: object
          properties:
            crew_assignments:
              type: array
              items:
                type: object
                properties:
                  id: { type: string }
                  position: { type: string }
                  start_date: { type: string, format: date-time }
                  is_primary: { type: boolean }
                  crew_member:
                    type: object
                    properties:
                      id: { type: string }
                      first_name: { type: string }
                      last_name: { type: string }
                      position:
                        oneOf: [{ type: string }, { type: "null" }]
            coi_requirements:
              type: array
              items:
                type: object
                properties:
                  id: { type: string }
                  credential_type_id: { type: string }
                  position: { type: string }
                  is_required: { type: boolean }

    CrewMember:
      type: object
      properties:
        id: { type: string }
        first_name: { type: string }
        last_name: { type: string }
        email:
          oneOf: [{ type: string, format: email }, { type: "null" }]
        phone:
          oneOf: [{ type: string }, { type: "null" }]
        mmc_number:
          oneOf: [{ type: string }, { type: "null" }]
        position:
          oneOf: [{ type: string }, { type: "null" }]
        hire_date:
          oneOf: [{ type: string, format: date-time }, { type: "null" }]
        credentials:
          type: array
          items: { $ref: "#/components/schemas/Credential" }

    CrewMemberDetail:
      allOf:
        - $ref: "#/components/schemas/CrewMember"
        - type: object
          properties:
            assignments:
              type: array
              items:
                type: object

    Credential:
      type: object
      properties:
        id: { type: string }
        type: { type: string }
        category:
          type: string
          enum: [LICENSE, STCW, TWIC, MEDICAL, DRUG_TEST, SAFETY, SEA_SERVICE, OTHER]
        document_number:
          oneOf: [{ type: string }, { type: "null" }]
        issue_date:
          oneOf: [{ type: string, format: date-time }, { type: "null" }]
        expiration_date:
          oneOf: [{ type: string, format: date-time }, { type: "null" }]
        issuing_authority:
          oneOf: [{ type: string }, { type: "null" }]
        verified: { type: boolean }

    CredentialExpiring:
      allOf:
        - $ref: "#/components/schemas/Credential"
        - type: object
          properties:
            days_left:
              oneOf: [{ type: integer }, { type: "null" }]
            crew_member:
              type: object
              properties:
                id: { type: string }
                first_name: { type: string }
                last_name: { type: string }

    Voyage:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        status:
          type: string
          enum: [PLANNED, UNDERWAY, COMPLETED, CANCELLED]
        departure_port: { type: string }
        arrival_port: { type: string }
        departure_time: { type: string, format: date-time }
        arrival_time:
          oneOf: [{ type: string, format: date-time }, { type: "null" }]
        actual_departure:
          oneOf: [{ type: string, format: date-time }, { type: "null" }]
        actual_arrival:
          oneOf: [{ type: string, format: date-time }, { type: "null" }]
        distance_nm:
          oneOf: [{ type: number }, { type: "null" }]
        fuel_budget_gal:
          oneOf: [{ type: number }, { type: "null" }]
        fuel_actual_gal:
          oneOf: [{ type: number }, { type: "null" }]
        revenue_cents:
          oneOf: [{ type: integer }, { type: "null" }]
        vessel:
          type: object
          properties:
            id: { type: string }
            name: { type: string }

    Incident:
      type: object
      properties:
        id: { type: string }
        vessel_id:
          oneOf: [{ type: string }, { type: "null" }]
        detected_at: { type: string, format: date-time }
        severity:
          type: string
          enum: [LOW, MEDIUM, HIGH, CRITICAL]
        status:
          type: string
          enum: [REPORTED, INVESTIGATING, CONTAINED, CLOSED, REPORTED_TO_NRC]
        summary: { type: string }
        affected_systems:
          type: array
          items: { type: string }
        reported_to_nrc_at:
          oneOf: [{ type: string, format: date-time }, { type: "null" }]
        nrc_case_number:
          oneOf: [{ type: string }, { type: "null" }]
        resolved_at:
          oneOf: [{ type: string, format: date-time }, { type: "null" }]

    InsurancePolicy:
      type: object
      properties:
        id: { type: string }
        vessel_id:
          oneOf: [{ type: string }, { type: "null" }]
        policy_number: { type: string }
        type: { type: string }
        insurer: { type: string }
        broker_name:
          oneOf: [{ type: string }, { type: "null" }]
        effective_from: { type: string, format: date-time }
        effective_to: { type: string, format: date-time }
        status: { type: string }
        policy_limit_usd:
          oneOf: [{ type: string }, { type: "null" }]
        deductible_usd:
          oneOf: [{ type: string }, { type: "null" }]
        premium_usd:
          oneOf: [{ type: string }, { type: "null" }]
        renewal_notice_days: { type: integer }
        auto_renew: { type: boolean }
        certificate_url:
          oneOf: [{ type: string, format: uri }, { type: "null" }]

    CyberPlan:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        vessel_id:
          oneOf: [{ type: string }, { type: "null" }]
        version: { type: string }
        cyso_name:
          oneOf: [{ type: string }, { type: "null" }]
        cyso_email:
          oneOf: [{ type: string, format: email }, { type: "null" }]
        next_assessment_due:
          oneOf: [{ type: string, format: date-time }, { type: "null" }]
        elements:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              key:
                type: string
                enum:
                  [
                    ACCOUNT_SECURITY,
                    DEVICE_SECURITY,
                    DATA_SECURITY,
                    GOVERNANCE_TRAINING,
                    RISK_MANAGEMENT,
                    SUPPLY_CHAIN,
                  ]
              status:
                type: string
                enum: [NOT_STARTED, IN_PROGRESS, COMPLETE]
        summary:
          type: object
          properties:
            total: { type: integer }
            complete: { type: integer }
            in_progress: { type: integer }
            not_started: { type: integer }
            percent_complete: { type: integer }

    ComplianceSummary:
      type: object
      properties:
        as_of: { type: string, format: date-time }
        overall_score: { type: integer, minimum: 0, maximum: 100 }
        grade: { type: string, enum: [A, B, C, D, F] }
        vessels:
          type: object
          properties:
            total: { type: integer }
            coi_current: { type: integer }
            coi_expired: { type: integer }
            coi_expiring_soon: { type: integer }
        crew:
          type: object
          properties:
            total: { type: integer }
        credentials:
          type: object
          properties:
            total: { type: integer }
            current: { type: integer }
            expiring_soon_90d: { type: integer }
            expiring_soon_30d: { type: integer }
            expired: { type: integer }

    # ─── Webhook event payloads ─────────────────────────────────────
    WebhookEnvelope:
      type: object
      required: [event, orgId, timestamp, data]
      properties:
        event:
          type: string
          enum:
            [
              CREDENTIAL_EXPIRING,
              CREDENTIAL_EXPIRED,
              DRILL_OVERDUE,
              DRILL_LOGGED,
              INCIDENT_CREATED,
              INCIDENT_CLOSED,
              COI_EXPIRING,
              CYBER_INCIDENT_REPORTED,
              WORK_REST_VIOLATION,
              OFAC_MATCH_FLAGGED,
              INSURANCE_RENEWAL_DUE,
              SURVEY_OVERDUE,
              PORT_CALL_PLANNED,
              CREW_ASSIGNED,
              VOYAGE_DEPARTED,
              VOYAGE_ARRIVED,
            ]
        orgId: { type: string }
        timestamp: { type: string, format: date-time }
        data:
          type: object
          description: Event-specific payload. See API docs for shape per event.

webhooks:
  binnacleEvent:
    post:
      summary: Outbound webhook from Binnacle to your endpoint
      description: |
        Binnacle POSTs every subscribed event to your URL with an HMAC-SHA256
        signature. Verify the signature with your subscription's secret before
        trusting the payload.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEnvelope" }
      parameters:
        - in: header
          name: X-Binnacle-Event
          required: true
          schema: { type: string }
        - in: header
          name: X-Binnacle-Delivery-Id
          required: true
          schema: { type: string }
        - in: header
          name: X-Binnacle-Signature
          required: true
          description: "Format: sha256=<hex digest of HMAC-SHA256(secret, body)>"
          schema: { type: string }
        - in: header
          name: X-Binnacle-Timestamp
          required: true
          schema: { type: string, format: date-time }
      responses:
        "200":
          description: Acknowledged. Binnacle treats any 2xx as success.
        "default":
          description: Non-2xx triggers a retry at 1m, 5m, 30m, 2h.
