{"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,\nkills, VS points (duels), donations, storm assignments, storm scores, and\ncustom event entries — and to export the same data in JSON or CSV.\n\n## Authentication\nInclude your API key in the `Authorization` header:\n```\nAuthorization: Bearer fops_live_<your-key>\n```\nKeys are scoped to a single alliance and carry a `READ` or `WRITE` scope.\nCreate and manage keys in the alliance settings under **API Access & Security**.\n\n## Rate limiting\n- 60 requests per minute per key\n- 5 000 requests per day per key\n\nEvery response includes `X-RateLimit-Remaining-Minute` and\n`X-RateLimit-Remaining-Day` headers. When exceeded you receive HTTP 429 with\na `Retry-After` header.\n\n## Errors\nAll errors follow a consistent shape:\n```json\n{ \"error\": { \"code\": \"RATE_LIMITED\", \"message\": \"...\", \"details\": {} } }\n```\nSwitch on `error.code` (stable) rather than `error.message`.\n\n## BigInt values\nPower and kill counters routinely exceed `Number.MAX_SAFE_INTEGER`. These\nfields are serialised as **strings** in JSON responses. Parse them\naccordingly (`BigInt(response.power)` in JS).\n","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\nckxyz…,PlayerOne,R4,ACTIVE,12500000,5200000,2026-01-01T00:00:00Z,\n"}}},"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\nckxyz…,PlayerOne,12500000,2026-04-16T10:00:00Z\n"}}}}},"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\nckxyz123,12500000\nckabc456,8300000\n"}}},"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\nckxyz123,5200000\n"}}},"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\nckxyz123,1500\n"}}},"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\nckxyz123,A,STARTER\nckabc456,B,SUBSTITUTE\n"}}},"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"}}}}}}