openapi: 3.1.0
info:
  title: FarmOps Public API
  version: 1.1.0
  description: >
    Programmatic access to FarmOps alliance data. Use this API to import power,

    kills, VS points (duels), donations, storm assignments, storm scores, and

    custom event entries — and to export the same data in JSON or CSV.


    ## Authentication

    Include your API key in the `Authorization` header:

    ```

    Authorization: Bearer fops_live_<your-key>

    ```

    Keys are scoped to a single alliance and carry a `READ` or `WRITE` scope.

    Create and manage keys in the alliance settings under **API Access &
    Security**.


    ## Rate limiting

    - 60 requests per minute per key

    - 5 000 requests per day per key


    Every response includes `X-RateLimit-Remaining-Minute` and

    `X-RateLimit-Remaining-Day` headers. When exceeded you receive HTTP 429 with

    a `Retry-After` header.


    ## Errors

    All errors follow a consistent shape:

    ```json

    { "error": { "code": "RATE_LIMITED", "message": "...", "details": {} } }

    ```

    Switch on `error.code` (stable) rather than `error.message`.


    ## BigInt values

    Power and kill counters routinely exceed `Number.MAX_SAFE_INTEGER`. These

    fields are serialised as **strings** in JSON responses. Parse them

    accordingly (`BigInt(response.power)` in JS).
  contact:
    name: FarmOps support
    email: support@lastwar.farm
servers:
  - url: https://www.lastwar.farm
    description: Current deployment
  - url: https://lastwar.farm
    description: Production
tags:
  - name: Alliance
  - name: Members
  - name: Storms
  - name: Custom Events
  - name: Seasons
  - name: Export
security:
  - bearerAuth: []
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: fops_live_...
  schemas:
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code
              enum:
                - UNAUTHENTICATED
                - INVALID_KEY
                - KEY_REVOKED
                - KEY_EXPIRED
                - INVALID_SCOPE
                - TRIAL_EXPIRED
                - RATE_LIMITED
                - VALIDATION_ERROR
                - MEMBER_NOT_FOUND
                - EVENT_NOT_FOUND
                - ALLIANCE_NOT_FOUND
                - DUPLICATE_ENTRY
                - UNSUPPORTED_FORMAT
                - INTERNAL_ERROR
            message:
              type: string
            details:
              description: Optional extra context; shape varies by error code.
    Member:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        role:
          type: string
          enum:
            - R1
            - R2
            - R3
            - R4
            - R5
        status:
          type: string
          enum:
            - ACTIVE
            - LEFT
            - EXCUSED
            - BANNED
        currentFightPower:
          type: string
          description: Stringified bigint
        currentKills:
          type: string
          description: Stringified bigint
        joinedAt:
          type: string
          format: date-time
        leftAt:
          type: string
          format: date-time
          nullable: true
    ImportResult:
      type: object
      properties:
        matchedCount:
          type: integer
        unmatchedCount:
          type: integer
        targetIds:
          type: array
          items:
            type: string
        activityLogId:
          type: string
          nullable: true
          description: Use this to undo the import within 24h via the web UI.
        unmatched:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              value: {}
  parameters:
    Format:
      name: format
      in: query
      schema:
        type: string
        enum:
          - json
          - csv
      description: Override the response format. Defaults to JSON. (CSV-capable
        endpoints only.)
  responses:
    RateLimitHeaders:
      description: Rate limit info is included in every response
      headers:
        X-RateLimit-Limit-Minute:
          schema:
            type: integer
        X-RateLimit-Remaining-Minute:
          schema:
            type: integer
        X-RateLimit-Limit-Day:
          schema:
            type: integer
        X-RateLimit-Remaining-Day:
          schema:
            type: integer
    Unauthorized:
      description: Missing, invalid, revoked, or expired key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: Key scope insufficient, or alliance trial expired
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ValidationError:
      description: Request body or params failed validation
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
paths:
  /api/v1/alliance:
    get:
      tags:
        - Alliance
      summary: Get alliance basic info
      operationId: getAlliance
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      slug:
                        type: string
                      trialEndsAt:
                        type: string
                        format: date-time
                      createdAt:
                        type: string
                        format: date-time
                      subscription:
                        type: object
                        nullable: true
                        properties:
                          status:
                            type: string
                          currentPeriodEnd:
                            type: string
                            format: date-time
                            nullable: true
                      settings:
                        type: object
                        properties:
                          weekStartDay:
                            type: integer
                            enum:
                              - 0
                              - 1
                          duelThreshold:
                            type: string
                          donationDaily:
                            type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/alliance/members:
    get:
      tags:
        - Members
      summary: List members
      operationId: listMembers
      parameters:
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: JSON array of members, or CSV file
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Member"
                  meta:
                    type: object
                    properties:
                      count:
                        type: integer
            text/csv:
              schema:
                type: string
              example: |
                id,name,role,status,currentFightPower,currentKills,joinedAt,leftAt
                ckxyz…,PlayerOne,R4,ACTIVE,12500000,5200000,2026-01-01T00:00:00Z,
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/alliance/members/power:
    get:
      tags:
        - Members
      summary: Export power history
      operationId: exportPower
      parameters:
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: Power history
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        memberId:
                          type: string
                        memberName:
                          type: string
                        power:
                          type: string
                        recordedAt:
                          type: string
                          format: date-time
            text/csv:
              schema:
                type: string
              example: |
                memberId,name,power,recordedAt
                ckxyz…,PlayerOne,12500000,2026-04-16T10:00:00Z
    post:
      tags:
        - Members
      summary: Import power values
      operationId: importPower
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - entries
              properties:
                entries:
                  type: array
                  items:
                    type: object
                    description: Each row identifies the member by `memberId` (preferred — skips
                      fuzzy matching) or `name` (fuzzy). At least one must be
                      provided; `memberId` wins if both are present.
                    required:
                      - power
                    properties:
                      memberId:
                        type: string
                        description: Exact alliance-member id. Preferred when known — skips fuzzy name
                          matching.
                      name:
                        type: string
                        description: Member name (case-insensitive, fuzzy matched). Ignored when
                          memberId is provided.
                      power:
                        type: number
                        description: Fight power value
                    anyOf:
                      - required:
                          - memberId
                      - required:
                          - name
            examples:
              byId:
                summary: Upload by memberId (preferred — no fuzzy match)
                value:
                  entries:
                    - memberId: ckxyz123
                      power: 12500000
                    - memberId: ckabc456
                      power: 8300000
              byName:
                summary: Upload by name (fuzzy matched)
                value:
                  entries:
                    - name: PlayerOne
                      power: 12500000
                    - name: PlayerTwo
                      power: 8300000
          text/csv:
            schema:
              type: string
            example: |
              memberId,power
              ckxyz123,12500000
              ckabc456,8300000
      responses:
        "200":
          description: Import summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ImportResult"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          description: Member not found (strict matching)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/alliance/members/kills:
    get:
      tags:
        - Members
      summary: Export kill history
      operationId: exportKills
      parameters:
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: Kill history
    post:
      tags:
        - Members
      summary: Import kill values
      operationId: importKills
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - entries
              properties:
                entries:
                  type: array
                  items:
                    type: object
                    required:
                      - kills
                    properties:
                      memberId:
                        type: string
                        description: Exact alliance-member id. Preferred when known.
                      name:
                        type: string
                        description: Member name (fuzzy matched). Ignored when memberId is provided.
                      kills:
                        type: number
                    anyOf:
                      - required:
                          - memberId
                      - required:
                          - name
          text/csv:
            schema:
              type: string
            example: |
              memberId,kills
              ckxyz123,5200000
      responses:
        "200":
          description: Import summary
  /api/v1/alliance/members/duels:
    get:
      tags:
        - Members
      summary: Export VS point scores
      operationId: exportDuels
      parameters:
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: Duel score history
    post:
      tags:
        - Members
      summary: Import VS point scores for a day
      operationId: importDuels
      parameters:
        - name: scoredOn
          in: query
          required: false
          schema:
            type: string
            format: date
          description: ISO date. Alternatively pass `scoredOn` in the JSON body.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - entries
              properties:
                scoredOn:
                  type: string
                  format: date
                entries:
                  type: array
                  items:
                    type: object
                    required:
                      - score
                    properties:
                      memberId:
                        type: string
                        description: Exact alliance-member id. Preferred when known.
                      name:
                        type: string
                        description: Member name (fuzzy matched). Ignored when memberId is provided.
                      score:
                        type: number
                    anyOf:
                      - required:
                          - memberId
                      - required:
                          - name
          text/csv:
            schema:
              type: string
            example: |
              memberId,score
              ckxyz123,1500
      responses:
        "200":
          description: Import summary
  /api/v1/alliance/members/donations:
    get:
      tags:
        - Members
      summary: Export weekly donation totals
      operationId: exportDonations
      parameters:
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: Donations
    post:
      tags:
        - Members
      summary: Import weekly donation totals
      operationId: importDonations
      parameters:
        - name: weekStart
          in: query
          required: false
          schema:
            type: string
            format: date
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - entries
              properties:
                weekStart:
                  type: string
                  format: date
                entries:
                  type: array
                  items:
                    type: object
                    required:
                      - total
                    properties:
                      memberId:
                        type: string
                        description: Exact alliance-member id. Preferred when known.
                      name:
                        type: string
                        description: Member name (fuzzy matched). Ignored when memberId is provided.
                      total:
                        type: number
                    anyOf:
                      - required:
                          - memberId
                      - required:
                          - name
          text/csv:
            schema:
              type: string
      responses:
        "200":
          description: Import summary
  /api/v1/alliance/storms:
    get:
      tags:
        - Storms
      summary: List storm events
      operationId: listStorms
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum:
              - CANYON
              - DESERT
        - name: from
          in: query
          schema:
            type: string
            format: date
        - name: to
          in: query
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Storm events
  /api/v1/alliance/storms/{eventId}/assignments:
    get:
      tags:
        - Storms
      summary: Get storm assignments for an event
      operationId: getStormAssignments
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: Assignments + scores
        "404":
          description: Storm event not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/alliance/storms/assignments:
    post:
      tags:
        - Storms
      summary: Import storm assignments (creates the event if needed)
      operationId: importStormAssignments
      parameters:
        - name: stormType
          in: query
          schema:
            type: string
            enum:
              - CANYON
              - DESERT
        - name: eventDate
          in: query
          schema:
            type: string
            format: date
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - entries
              properties:
                stormType:
                  type: string
                  enum:
                    - CANYON
                    - DESERT
                eventDate:
                  type: string
                  format: date
                entries:
                  type: array
                  items:
                    type: object
                    required:
                      - team
                      - role
                    properties:
                      memberId:
                        type: string
                        description: Exact alliance-member id. Preferred when known.
                      name:
                        type: string
                        description: Member name (fuzzy matched). Ignored when memberId is provided.
                      team:
                        type: string
                        enum:
                          - A
                          - B
                      role:
                        type: string
                        enum:
                          - STARTER
                          - SUBSTITUTE
                    anyOf:
                      - required:
                          - memberId
                      - required:
                          - name
          text/csv:
            schema:
              type: string
            example: |
              memberId,team,role
              ckxyz123,A,STARTER
              ckabc456,B,SUBSTITUTE
      responses:
        "200":
          description: Import summary
  /api/v1/alliance/storms/scores:
    post:
      tags:
        - Storms
      summary: Import storm scores after an event
      operationId: importStormScores
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - entries
                - stormType
                - eventDate
              properties:
                stormType:
                  type: string
                  enum:
                    - CANYON
                    - DESERT
                eventDate:
                  type: string
                  format: date
                defaultTeam:
                  type: string
                  enum:
                    - A
                    - B
                entries:
                  type: array
                  items:
                    type: object
                    required:
                      - score
                    properties:
                      memberId:
                        type: string
                        description: Exact alliance-member id. Preferred when known.
                      name:
                        type: string
                        description: Member name (fuzzy matched). Ignored when memberId is provided.
                      score:
                        type: number
                    anyOf:
                      - required:
                          - memberId
                      - required:
                          - name
          text/csv:
            schema:
              type: string
      responses:
        "200":
          description: Import summary
  /api/v1/alliance/custom-events:
    get:
      tags:
        - Custom Events
      summary: List custom events
      operationId: listCustomEvents
      responses:
        "200":
          description: Events
  /api/v1/alliance/custom-events/{eventId}/entries:
    get:
      tags:
        - Custom Events
      summary: Export entries for a custom event
      operationId: getCustomEventEntries
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: Entries
    post:
      tags:
        - Custom Events
      summary: Import entries for a custom event
      operationId: importCustomEventEntries
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
        - name: recordedAt
          in: query
          schema:
            type: string
            format: date-time
        - name: metricId
          in: query
          schema:
            type: string
        - name: phase
          in: query
          schema:
            type: string
            enum:
              - before
              - after
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - entries
              properties:
                recordedAt:
                  type: string
                  format: date-time
                metricId:
                  type: string
                phase:
                  type: string
                  enum:
                    - before
                    - after
                entries:
                  type: array
                  items:
                    type: object
                    required:
                      - value
                    properties:
                      memberId:
                        type: string
                        description: Exact alliance-member id. Preferred when known.
                      name:
                        type: string
                        description: Member name (fuzzy matched). Ignored when memberId is provided.
                      value:
                        type: number
                    anyOf:
                      - required:
                          - memberId
                      - required:
                          - name
          text/csv:
            schema:
              type: string
      responses:
        "200":
          description: Import summary
  /api/v1/alliance/seasons:
    get:
      tags:
        - Seasons
      summary: List seasons
      operationId: listSeasons
      responses:
        "200":
          description: Seasons
  /api/v1/alliance/export:
    get:
      tags:
        - Export
      summary: Export everything as one JSON bundle
      description: Full alliance snapshot (members + history + storms + custom events
        + seasons). Large — cache the result.
      operationId: exportAll
      responses:
        "200":
          description: Bundle
