openapi: 3.1.0
info:
  title: DSS Public API
  description: Public OAuth 2.0 API for Digital Sea Service. See https://docs.digitalseaservice.com
    for the developer guide.
  version: 1.0.0
  contact:
    name: DSS Developer Support
    email: admin@digitalseaservice.com
    url: https://docs.digitalseaservice.com
  license:
    name: Proprietary
  termsOfService: https://digitalseaservice.com/terms
paths:
  /oauth/authorize:
    get:
      tags:
      - oauth
      summary: Authorize Get
      operationId: authorize_get_oauth_authorize_get
      parameters:
      - name: response_type
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Response Type
      - name: client_id
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Client Id
      - name: redirect_uri
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Redirect Uri
      - name: scope
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Scope
      - name: state
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: State
      - name: code_challenge
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Code Challenge
      - name: code_challenge_method
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          default: S256
          title: Code Challenge Method
      - name: user_hint
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: User Hint
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    post:
      tags:
      - oauth
      summary: Authorize Post
      operationId: authorize_post_oauth_authorize_post
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/Body_authorize_post_oauth_authorize_post'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /oauth/token:
    post:
      tags:
      - oauth
      summary: Token Endpoint
      operationId: token_endpoint_oauth_token_post
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/Body_token_endpoint_oauth_token_post'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /oauth/revoke:
    post:
      tags:
      - oauth
      summary: Revoke Endpoint
      operationId: revoke_endpoint_oauth_revoke_post
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/Body_revoke_endpoint_oauth_revoke_post'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /oauth/userinfo:
    get:
      tags:
      - oauth
      summary: Userinfo Endpoint
      operationId: userinfo_endpoint_oauth_userinfo_get
      responses:
        '200':
          description: Basic profile for the authenticated user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInfo'
  /v1/me:
    get:
      tags:
      - v1
      summary: Get the authenticated user's profile
      operationId: get_me_v1_me_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
        '401':
          description: "`invalid_token` \u2014 bearer token missing, malformed, expired,\
            \ or revoked."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '403':
          description: "`insufficient_scope` \u2014 the token lacks the required scope."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '429':
          description: "`rate_limit_exceeded` \u2014 per-client request budget exhausted."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '404':
          description: "`not_found` \u2014 no matching record for the authenticated\
            \ user."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '500':
          description: "`internal_error` \u2014 an unexpected error occurred; reference\
            \ `instance`."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
      security:
      - oauth2:
        - profile:read
      - bearerAuth: []
  /v1/me/sea-time:
    get:
      tags:
      - sea-time
      summary: Get aggregate sea-time totals
      operationId: get_sea_time_v1_me_sea_time_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeaTimeResponse'
        '304':
          description: Resource has not changed since the conditional header.
        '401':
          description: "`invalid_token` \u2014 bearer token missing, malformed, expired,\
            \ or revoked."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '403':
          description: "`insufficient_scope` \u2014 the token lacks the required scope."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '429':
          description: "`rate_limit_exceeded` \u2014 per-client request budget exhausted."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '404':
          description: "`not_found` \u2014 no matching record for the authenticated\
            \ user."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '500':
          description: "`internal_error` \u2014 an unexpected error occurred; reference\
            \ `instance`."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
      security:
      - oauth2:
        - seatime:read
      - bearerAuth: []
  /v1/me/sea-time/recent:
    get:
      tags:
      - sea-time
      summary: Get the rolling 12-month sea-time trend
      operationId: get_sea_time_recent_v1_me_sea_time_recent_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeaTimeRecentResponse'
        '304':
          description: Resource has not changed since the conditional header.
        '401':
          description: "`invalid_token` \u2014 bearer token missing, malformed, expired,\
            \ or revoked."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '403':
          description: "`insufficient_scope` \u2014 the token lacks the required scope."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '429':
          description: "`rate_limit_exceeded` \u2014 per-client request budget exhausted."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '404':
          description: "`not_found` \u2014 no matching record for the authenticated\
            \ user."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '500':
          description: "`internal_error` \u2014 an unexpected error occurred; reference\
            \ `instance`."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
      security:
      - oauth2:
        - seatime:read
      - bearerAuth: []
  /v1/me/vessels:
    get:
      tags:
      - vessels
      summary: List the authenticated user's vessel history
      operationId: list_vessels_v1_me_vessels_get
      parameters:
      - name: cursor
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Opaque cursor from a prior response.
          title: Cursor
        description: Opaque cursor from a prior response.
      - name: limit
        in: query
        required: false
        schema:
          anyOf:
          - type: integer
            maximum: 200
            minimum: 1
          - type: 'null'
          description: Page size; max 200.
          title: Limit
        description: Page size; max 200.
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VesselsResponse'
        '304':
          description: Resource has not changed since the conditional header.
        '422':
          description: "`invalid_request` \u2014 request validation failed; see the\
            \ `errors` array."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ValidationProblemDetails'
        '401':
          description: "`invalid_token` \u2014 bearer token missing, malformed, expired,\
            \ or revoked."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '403':
          description: "`insufficient_scope` \u2014 the token lacks the required scope."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '429':
          description: "`rate_limit_exceeded` \u2014 per-client request budget exhausted."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '400':
          description: "`invalid_request` \u2014 a query parameter (e.g. cursor) was\
            \ malformed."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '500':
          description: "`internal_error` \u2014 an unexpected error occurred; reference\
            \ `instance`."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
      security:
      - oauth2:
        - vessels:read
      - bearerAuth: []
  /v1/webhooks/test:
    get:
      tags:
      - webhooks
      summary: Fire a webhook.test event to your registered URL
      operationId: fire_test_webhook_v1_webhooks_test_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestResponse'
        '401':
          description: "`invalid_token` \u2014 bearer token missing, malformed, expired,\
            \ or revoked."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '403':
          description: "`partner_suspended` \u2014 the calling partner has been suspended."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '404':
          description: "`not_found` \u2014 the partner record backing the token was\
            \ not found."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '409':
          description: "`no_webhook_configured` \u2014 no webhook URL or signing secret\
            \ is registered."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '500':
          description: "`internal_error` \u2014 an unexpected error occurred; reference\
            \ `instance`."
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
      security:
      - bearerAuth: []
      - oauth2: []
components:
  schemas:
    Body_authorize_post_oauth_authorize_post:
      properties:
        action:
          type: string
          title: Action
        client_id:
          type: string
          title: Client Id
        redirect_uri:
          type: string
          title: Redirect Uri
        response_type:
          type: string
          title: Response Type
          default: code
        scope:
          type: string
          title: Scope
          default: ''
        state:
          type: string
          title: State
          default: ''
        code_challenge:
          type: string
          title: Code Challenge
        code_challenge_method:
          type: string
          title: Code Challenge Method
          default: S256
        user_id:
          anyOf:
          - type: string
          - type: 'null'
          title: User Id
        _admin_token:
          anyOf:
          - type: string
          - type: 'null'
          title: Admin Token
      type: object
      required:
      - action
      - client_id
      - redirect_uri
      - code_challenge
      title: Body_authorize_post_oauth_authorize_post
    Body_revoke_endpoint_oauth_revoke_post:
      properties:
        token:
          anyOf:
          - type: string
          - type: 'null'
          title: Token
        token_type_hint:
          anyOf:
          - type: string
          - type: 'null'
          title: Token Type Hint
        client_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Client Id
        client_secret:
          anyOf:
          - type: string
          - type: 'null'
          title: Client Secret
      type: object
      title: Body_revoke_endpoint_oauth_revoke_post
    Body_token_endpoint_oauth_token_post:
      properties:
        grant_type:
          anyOf:
          - type: string
          - type: 'null'
          title: Grant Type
        code:
          anyOf:
          - type: string
          - type: 'null'
          title: Code
        redirect_uri:
          anyOf:
          - type: string
          - type: 'null'
          title: Redirect Uri
        client_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Client Id
        client_secret:
          anyOf:
          - type: string
          - type: 'null'
          title: Client Secret
        code_verifier:
          anyOf:
          - type: string
          - type: 'null'
          title: Code Verifier
        refresh_token:
          anyOf:
          - type: string
          - type: 'null'
          title: Refresh Token
        scope:
          anyOf:
          - type: string
          - type: 'null'
          title: Scope
      type: object
      title: Body_token_endpoint_oauth_token_post
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    MeResponse:
      properties:
        user_id:
          type: string
          title: User Id
          description: Stable DSS user identifier.
        name:
          type: string
          title: Name
        role:
          anyOf:
          - type: string
          - type: 'null'
          title: Role
          description: The crew member's current rank/position.
        country:
          anyOf:
          - type: string
          - type: 'null'
          title: Country
          description: "Country of residence. Always ``null`` in V1 \u2014 DSS does\
            \ not yet carry this on the user record. See docs/internal/data-model-gaps.md."
        photo_url:
          anyOf:
          - type: string
          - type: 'null'
          title: Photo Url
          description: "Profile photo URL. Always ``null`` in V1 \u2014 DSS stores\
            \ avatars in a private bucket with no public URL. See docs/internal/data-model-gaps.md."
        record_updated_at:
          type: string
          format: date-time
          title: Record Updated At
      type: object
      required:
      - user_id
      - name
      - record_updated_at
      title: MeResponse
      description: The authenticated user's profile.
      example:
        name: Alex Crew
        record_updated_at: '2026-05-26T09:14:00Z'
        role: Chief Officer
        user_id: sFmjAD7I8bQexmTiTbZZQwq10X43
    SeaTimeRecentResponse:
      properties:
        user_id:
          type: string
          title: User Id
        months:
          items:
            $ref: '#/components/schemas/_RecentMonth'
          type: array
          title: Months
          description: Most-recent month first; max 12.
        record_updated_at:
          type: string
          format: date-time
          title: Record Updated At
      type: object
      required:
      - user_id
      - months
      - record_updated_at
      title: SeaTimeRecentResponse
      description: Rolling 12-month trend of declared sea time.
      example:
        months:
        - days_at_sea: 18
          month: 2026-05
          nautical_miles: 1240
        - days_at_sea: 22
          month: 2026-04
          nautical_miles: 1480
        record_updated_at: '2026-05-22T14:21:00Z'
        user_id: 65a1f0e2c3b4d5e6f7a8b9c0
    SeaTimeResponse:
      properties:
        user_id:
          type: string
          title: User Id
        totals:
          $ref: '#/components/schemas/_DeclaredTotals'
        verified:
          $ref: '#/components/schemas/_VerifiedTotals'
        by_role:
          items:
            $ref: '#/components/schemas/_RoleBreakdown'
          type: array
          title: By Role
        record_updated_at:
          type: string
          format: date-time
          title: Record Updated At
      type: object
      required:
      - user_id
      - totals
      - verified
      - by_role
      - record_updated_at
      title: SeaTimeResponse
      description: Aggregate sea-time totals for the authenticated user.
      example:
        by_role:
        - days: 320
          role: Deckhand
        - days: 480
          role: Bosun
        - days: 1042
          role: Mate
        record_updated_at: '2026-05-22T14:21:00Z'
        totals:
          days_at_sea: 1842
          nautical_miles: 87423
          vessels_served: 14
          years_active: 6.2
        user_id: 65a1f0e2c3b4d5e6f7a8b9c0
        verified:
          days_at_sea: 1680
          nautical_miles: 79215
    TestResponse:
      properties:
        event_id:
          type: string
          title: Event Id
        delivery_id:
          type: string
          title: Delivery Id
        target_url:
          type: string
          title: Target Url
      type: object
      required:
      - event_id
      - delivery_id
      - target_url
      title: TestResponse
      description: Result of firing a ``webhook.test`` event.
      example:
        delivery_id: dlv_77f1bb31c0c64e6db4f5e8a9c2d3e4f5
        event_id: evt_3f4a9c8e2b1d4f5a8c9e0d1f2a3b4c5d
        target_url: https://yachtworkerscouncil.com/integrations/dss/webhook
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
            - type: string
            - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
      - loc
      - msg
      - type
      title: ValidationError
    VesselsResponse:
      properties:
        user_id:
          type: string
          title: User Id
        vessels:
          items:
            $ref: '#/components/schemas/_VesselPeriod'
          type: array
          title: Vessels
        next_cursor:
          anyOf:
          - type: string
          - type: 'null'
          title: Next Cursor
          description: Opaque cursor for the next page. ``null`` when the page is
            the last one.
        record_updated_at:
          type: string
          format: date-time
          title: Record Updated At
          description: "Most-recently-updated crew-list record for the user. Same\
            \ value across every page of a stable scan \u2014 drives the page-stable\
            \ ``ETag``."
      type: object
      required:
      - user_id
      - vessels
      - record_updated_at
      title: VesselsResponse
      description: Page of vessel periods for the authenticated user.
      example:
        next_cursor: eyJsYXN0X2lkIjoiMDFIVjlYS...
        record_updated_at: '2026-05-22T14:21:00Z'
        user_id: 65a1f0e2c3b4d5e6f7a8b9c0
        vessels:
        - days: 99
          id: 69961ffbbd7419b471faab60
          imo: '1012725'
          name: M/V Endeavour
          nautical_miles: 4280
          period_end: '2025-04-20T00:00:00Z'
          period_start: '2025-01-12T00:00:00Z'
          role: Chief Officer
    _DeclaredTotals:
      properties:
        days_at_sea:
          type: integer
          minimum: 0.0
          title: Days At Sea
          description: Total days the crew has logged at sea.
        nautical_miles:
          type: integer
          minimum: 0.0
          title: Nautical Miles
          description: Total nautical miles travelled.
        vessels_served:
          type: integer
          minimum: 0.0
          title: Vessels Served
          description: Distinct vessels served.
        years_active:
          type: number
          minimum: 0.0
          title: Years Active
          description: Earliest-to-latest span in years.
      type: object
      required:
      - days_at_sea
      - nautical_miles
      - vessels_served
      - years_active
      title: _DeclaredTotals
      description: Crew-declared aggregate sea time.
      example:
        days_at_sea: 1842
        nautical_miles: 87423
        vessels_served: 14
        years_active: 6.2
    _RecentMonth:
      properties:
        month:
          type: string
          pattern: ^\d{4}-(0[1-9]|1[0-2])$
          title: Month
          description: ISO year-month.
        days_at_sea:
          type: integer
          minimum: 0.0
          title: Days At Sea
        nautical_miles:
          type: integer
          minimum: 0.0
          title: Nautical Miles
      type: object
      required:
      - month
      - days_at_sea
      - nautical_miles
      title: _RecentMonth
      description: One month's contribution to the rolling 12-month trend.
      example:
        days_at_sea: 18
        month: 2026-05
        nautical_miles: 1240
    _RoleBreakdown:
      properties:
        role:
          type: string
          title: Role
        days:
          type: integer
          minimum: 0.0
          title: Days
      type: object
      required:
      - role
      - days
      title: _RoleBreakdown
      description: Days at sea served in a single role.
      example:
        days: 480
        role: Bosun
    _VerifiedTotals:
      properties:
        days_at_sea:
          type: integer
          minimum: 0.0
          title: Days At Sea
        nautical_miles:
          type: integer
          minimum: 0.0
          title: Nautical Miles
      type: object
      required:
      - days_at_sea
      - nautical_miles
      title: _VerifiedTotals
      description: Independently confirmed subset of the declared totals.
      example:
        days_at_sea: 1680
        nautical_miles: 79215
    _VesselPeriod:
      properties:
        id:
          type: string
          title: Id
          description: Stable identifier for this vessel period (crew-list id).
        name:
          type: string
          title: Name
        imo:
          anyOf:
          - type: string
          - type: 'null'
          title: Imo
          description: IMO number as a string when DSS holds it, else ``null``.
        role:
          anyOf:
          - type: string
          - type: 'null'
          title: Role
          description: Rank/position served in this period.
        period_start:
          type: string
          format: date-time
          title: Period Start
        period_end:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Period End
          description: '``null`` while still aboard.'
        days:
          type: integer
          minimum: 0.0
          title: Days
          description: Qualifying sea-service days logged in the period.
        nautical_miles:
          type: integer
          minimum: 0.0
          title: Nautical Miles
      type: object
      required:
      - id
      - name
      - period_start
      - days
      - nautical_miles
      title: _VesselPeriod
      description: A single period the crew member served on a vessel.
      example:
        days: 99
        id: 69961ffbbd7419b471faab60
        imo: '1012725'
        name: M/V Endeavour
        nautical_miles: 4280
        period_end: '2025-04-20T00:00:00Z'
        period_start: '2025-01-12T00:00:00Z'
        role: Chief Officer
    ProblemDetails:
      type: object
      description: RFC 7807 Problem Details. Returned with the `application/problem+json`
        content type on every resource (`/v1/*`) error.
      properties:
        type:
          type: string
          format: uri
          description: Stable URL identifying the error type; resolves to its docs
            page.
        title:
          type: string
          description: Short, human-readable summary of the error type.
        status:
          type: integer
          description: HTTP status code.
        detail:
          type: string
          description: Explanation specific to this occurrence.
        instance:
          type: string
          description: Request correlation id; matches the `X-Request-Id` header.
      required:
      - type
      - title
      - status
      - detail
      - instance
    ValidationProblemDetails:
      description: Problem Details for a 422, carrying a per-field `errors` array.
      allOf:
      - $ref: '#/components/schemas/ProblemDetails'
      - type: object
        properties:
          errors:
            type: array
            description: One entry per field that failed validation.
            items:
              type: object
              additionalProperties: true
    UserInfo:
      type: object
      description: OIDC-style basic profile returned by `/oauth/userinfo`. Profile
        fields are present only when the token carries the `profile:read` scope.
      properties:
        sub:
          type: string
          description: Stable DSS user identifier (subject).
        name:
          type:
          - string
          - 'null'
        role:
          type:
          - string
          - 'null'
        country:
          type:
          - string
          - 'null'
        picture:
          type:
          - string
          - 'null'
          description: Profile photo URL.
      required:
      - sub
  securitySchemes:
    oauth2:
      type: oauth2
      description: OAuth 2.0 Authorization Code flow with PKCE. See the integration
        guide at docs.digitalseaservice.com for the full step-by-step.
      flows:
        authorizationCode:
          authorizationUrl: http://localhost:8080/oauth/authorize
          tokenUrl: http://localhost:8080/oauth/token
          refreshUrl: http://localhost:8080/oauth/token
          scopes:
            profile:read: Read the crew member's name, role, country, and photo URL.
            seatime:read: Read aggregate sea-time totals and the 12-month trend.
            vessels:read: Read the crew member's vessel history.
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Opaque
servers:
- url: https://api.digitalseaservice.com
  description: Production
- url: https://api.dev.digitalseaservice.com
  description: Sandbox (dev)
