{"openapi":"3.0.3","info":{"title":"ExpLint REST API","version":"0.1.0","description":"ExpLint lints A/B test specs against the rules a team writes. This REST API creates and manages the projects and lint rules one organization holds, and lints a spec against the rules a project has switched on. Send an API key as a Bearer [REDACTED], or an OAuth access token granted one of the scopes each operation names, and the credential names the organization the request works in, so no call names one of its own. This is the REST reading of the MCP server at /mcp, and the two answer with the same result shapes."},"servers":[{"url":"https://www.explint.com"}],"tags":[{"name":"Projects","description":"ExpLint projects in one organization."},{"name":"Rules","description":"Lint rules inside an ExpLint project."},{"name":"Lint","description":"Lint a spec against a project's rules."},{"name":"Workspace","description":"Endpoints the signed-in ExpLint workspace calls with an Appwrite session JWT."}],"paths":{"/api/projects":{"get":{"tags":["Projects"],"summary":"List projects","description":"The organization's projects, standing in title order, one page at a time. A page holds up to 100 rows and 25 when limit is left out, and nextCursor in an answer is what the next call passes as cursor to continue where the listing stopped.","operationId":"listProjects","security":[{"oauth2":["projects.read"]},{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"query","description":"Keep only the projects whose slug contains this text, ignoring case.","schema":{"type":"string"}},{"name":"includeArchived","in":"query","description":"Bring archived projects back into the listing when this is true, since they stay out of it otherwise.","schema":{"type":"boolean"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"}],"responses":{"200":{"description":"One page of projects.","content":{"application/json":{"schema":{"type":"object","properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"nextCursor":{"type":"string","nullable":true,"description":"The cursor the next call passes to continue, and null once the listing reached its end."}},"required":["projects","nextCursor"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Projects"],"summary":"Create a project","description":"A project named by its title, which generates the slug every later call names it by. A name another project already holds in this organization is refused, and adding a custom project needs prepaid credits, so an organization that has never topped up at least $5 comes back with a 402.","operationId":"createProject","security":[{"oauth2":["projects.write"]},{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"The name for the new project."}},"required":["name"]}}}},"responses":{"201":{"description":"The project as it was written.","content":{"application/json":{"schema":{"type":"object","properties":{"project":{"$ref":"#/components/schemas/Project"}},"required":["project"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/projects/{name}":{"parameters":[{"$ref":"#/components/parameters/projectName"}],"patch":{"tags":["Projects"],"summary":"Rename or archive a project","description":"Renames the project when newName is sent and regenerates its slug with the new name, so a name another project already holds in this organization is refused, and archives or restores the project when archived is sent as true or false.","operationId":"renameProject","security":[{"oauth2":["projects.write"]},{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"newName":{"type":"string","description":"The name to rename it to."},"archived":{"type":"boolean","description":"Whether the project is archived, which keeps its rules and lint history in place while it leaves the working list, and false restores an archived project."}},"required":[]}}}},"responses":{"200":{"description":"The project as it was written.","content":{"application/json":{"schema":{"type":"object","properties":{"project":{"$ref":"#/components/schemas/Project"}},"required":["project"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Projects"],"summary":"Archive a project","description":"Archives the project, which keeps its rules and lint history in place for reporting and takes it out of the working list. Projects are never deleted, and restoring one is a PATCH carrying archived as false. The Default project an organization starts with cannot be archived.","operationId":"archiveProject","security":[{"oauth2":["projects.write"]},{"bearerAuth":[]}],"responses":{"200":{"description":"The project is archived.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The project refuses archiving, or the token lacks the scope this operation names.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/projects/{projectSlug}/rules":{"parameters":[{"$ref":"#/components/parameters/projectSlug"}],"get":{"tags":["Rules"],"summary":"List rules","description":"One project's rules, standing in creation order, one page at a time. A page holds up to 100 rows and 25 when limit is left out, and nextCursor in an answer is what the next call passes as cursor to continue where the listing stopped.","operationId":"listRules","security":[{"oauth2":["projects.read"]},{"bearerAuth":[]}],"parameters":[{"name":"ruleKey","in":"query","description":"Keep only the rules whose key contains this text, ignoring case.","schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"}],"responses":{"200":{"description":"One page of rules.","content":{"application/json":{"schema":{"type":"object","properties":{"rules":{"type":"array","items":{"$ref":"#/components/schemas/Rule"}},"nextCursor":{"type":"string","nullable":true,"description":"The cursor the next call passes to continue, and null once the listing reached its end."}},"required":["rules","nextCursor"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Rules"],"summary":"Create a rule","description":"A rule carries a key that identifies it later, a title, and the instruction the lint model judges the content against. A project carries at most 50 rules, and a key another rule in the project already holds is refused. Adding a rule needs prepaid credits, so an organization that has never topped up at least $5 comes back with a 402, while turning a rule a project already carries on or off stays open to every organization as long as one rule stays turned on.","operationId":"createRule","security":[{"oauth2":["projects.write"]},{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ruleKey":{"type":"string","description":"The key for the new rule."},"ruleTitle":{"type":"string","description":"The rule title."},"ruleInstruction":{"type":"string","description":"What the rule checks, in plain English."},"severity":{"$ref":"#/components/schemas/Severity"},"enabled":{"type":"boolean","description":"Whether the rule is on. A new rule starts on."}},"required":["ruleKey","ruleTitle","ruleInstruction"]}}}},"responses":{"201":{"description":"The rule as it was written.","content":{"application/json":{"schema":{"type":"object","properties":{"rule":{"$ref":"#/components/schemas/Rule"}},"required":["rule"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/projects/{projectSlug}/rules/{ruleKey}":{"parameters":[{"$ref":"#/components/parameters/projectSlug"},{"$ref":"#/components/parameters/ruleKey"}],"patch":{"tags":["Rules"],"summary":"Change a rule","description":"Changes whichever of the fields the body carries, beside the key that identifies the rule. Sending a body with none of them is refused.","operationId":"changeRule","security":[{"oauth2":["projects.write"]},{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ruleTitle":{"type":"string","description":"The title to change it to."},"newRuleKey":{"type":"string","description":"The key to rename the rule to."},"ruleInstruction":{"type":"string","description":"The instruction to change it to."},"severity":{"$ref":"#/components/schemas/Severity"},"enabled":{"type":"boolean","description":"Whether the rule is on."}}}}}},"responses":{"200":{"description":"The rule as it was written.","content":{"application/json":{"schema":{"type":"object","properties":{"rule":{"$ref":"#/components/schemas/Rule"}},"required":["rule"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Rules"],"summary":"Remove a rule","description":"Removes the rule from its project.","operationId":"removeRule","security":[{"oauth2":["projects.write"]},{"bearerAuth":[]}],"responses":{"200":{"description":"The rule is gone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/projects/{projectSlug}/lint":{"parameters":[{"$ref":"#/components/parameters/projectSlug"}],"post":{"tags":["Lint"],"summary":"Lint a spec","description":"Checks one piece of content against every rule the project has switched on, and answers with each rule placed in errors, warnings, or passed by the severity the rule carries, together with the probability the model gave the content of satisfying it and the bad and good examples the rule carries where it has them. quality_score is the share of the rules that passed. A rule switched off reports against nothing.","operationId":"lintProject","security":[{"oauth2":["lint.run"]},{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string","description":"The content to check, usually a spec as markdown, which can also be HTML or plain text. An image embedded in it as base64 is dropped before it is judged, and content beyond the acceptable size comes back as an error."}},"required":["content"]}}}},"responses":{"200":{"description":"The lint result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LintResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"The organization has no lint requests left.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"502":{"description":"The linter could not answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/lint":{"post":{"tags":["Workspace"],"summary":"Lint a spec from the workspace Test tab","description":"Scores a project's turned-on rules against a spec for a caller with an Appwrite session, without recording a lint request. The workspace rate limit is five specs per organization in six hours.","operationId":"lintFromWorkspace","security":[{"appwriteJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string"},"content":{"type":"string"}},"required":["projectId","content"]}}}},"responses":{"200":{"description":"The lint result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LintResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"Too many lint requests in the current window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The linter could not answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/keys":{"post":{"tags":["Workspace"],"summary":"Mint an API key from the workspace","description":"Creates an abl_live_ API key for an organization the caller belongs to, returning the plaintext token once. The stored row holds only its digest.","operationId":"mintApiKey","security":[{"appwriteJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"orgId":{"type":"string"},"name":{"type":"string","maxLength":128}},"required":["orgId","name"]}}}},"responses":{"200":{"description":"The minted key row and its one-time token.","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"$ref":"#/components/schemas/MintedKey"},"token":{"type":"string"}},"required":["key","token"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"description":"Too many keys minted in the current window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/checkout":{"post":{"tags":["Workspace"],"summary":"Open a credit checkout from the workspace","description":"Opens a hosted Polar checkout for a credit top-up of at least $5 for an organization the caller owns or administers.","operationId":"createCheckout","security":[{"appwriteJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"orgId":{"type":"string"},"amount":{"type":"integer","minimum":5,"description":"The top-up in whole dollars."}},"required":["orgId","amount"]}}}},"responses":{"200":{"description":"The checkout URL to send the buyer to.","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"}},"required":["url"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"description":"Too many checkouts opened in the current window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The checkout could not be started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key from the organization's MCP page, one carrying an abl_live_ prefix. The key names the organization every request works in and carries full access to that organization. This API takes no session cookie."},"oauth2":{"type":"oauth2","description":"An OAuth access token from the ExpLint authorization server. Request only the scope an operation names so the token carries least-privilege access, and read scopes_supported at /.well-known/oauth-protected-resource/api for the same list. A token granted projects.write before the narrower scopes existed also satisfies projects.read and lint.run.","flows":{"authorizationCode":{"authorizationUrl":"https://fra.cloud.appwrite.io/v1/oauth2/6ad74e8a001b0473ed32/authorize","tokenUrl":"https://fra.cloud.appwrite.io/v1/oauth2/6ad74e8a001b0473ed32/token","scopes":{"projects.read":"List ExpLint projects and the rules inside them.","projects.write":"Create, change, and archive ExpLint projects and the rules inside them.","lint.run":"Lint a spec against the rules a project has switched on, spending one lint request."}}}},"appwriteJwt":{"type":"http","scheme":"bearer","description":"An Appwrite session JWT the signed-in workspace mints for its session. These endpoints answer the workspace alone and take no API key or OAuth token."}},"parameters":{"projectName":{"name":"name","in":"path","required":true,"description":"The project's name or slug, percent-encoded and matched ignoring case.","schema":{"type":"string"}},"projectSlug":{"name":"projectSlug","in":"path","required":true,"description":"The project's name or slug, percent-encoded and matched ignoring case.","schema":{"type":"string"}},"ruleKey":{"name":"ruleKey","in":"path","required":true,"description":"The rule's key, percent-encoded and matched ignoring case. A key may hold slashes, so encode them: the server reads everything after /rules as the key, and the key SPEC/no-unknown-flags works spelled spec%2Fno-unknown-flags too.","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"How many rows one answer carries, from 1 to 100, and 25 when this is left out.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},"cursor":{"name":"cursor","in":"query","description":"The nextCursor an earlier answer returned, so the listing continues where it stopped.","schema":{"type":"string"}}},"responses":{"BadRequest":{"description":"The request was refused: read the message and fix what it names.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"No valid credential arrived with the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The credential may not do this, or an OAuth token lacks the scope this operation names.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"The organization holds no such project or rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"A project name or rule key the request writes already exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Project":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"archived":{"type":"boolean"}},"required":["id","name","slug","archived"]},"Rule":{"type":"object","properties":{"id":{"type":"string"},"ruleTitle":{"type":"string"},"ruleKey":{"type":"string"},"ruleInstruction":{"type":"string"},"severity":{"$ref":"#/components/schemas/Severity"},"enabled":{"type":"boolean"}},"required":["id","ruleTitle","ruleKey","ruleInstruction","severity","enabled"]},"RuleFinding":{"type":"object","properties":{"title":{"type":"string"},"key":{"type":"string"},"description":{"type":"string"},"probability":{"type":"number","description":"The probability the model gave the content of satisfying the rule, from 0 to 1. A score of 0.8 or more passed."},"bad_example":{"type":"string","nullable":true,"description":"A short excerpt from a spec that breaks the rule, where the rule carries one, so the caller can see what a break looks like beside the finding."},"good_example":{"type":"string","nullable":true,"description":"A short excerpt from a spec that passes the rule, where the rule carries one, so the caller can see what the content needs to change towards."}},"required":["title","key","description","probability","bad_example","good_example"]},"LintResult":{"type":"object","properties":{"quality_score":{"type":"number","description":"The share of the rules that passed, to three decimals."},"errors":{"type":"array","items":{"$ref":"#/components/schemas/RuleFinding"}},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/RuleFinding"}},"passed":{"type":"array","items":{"$ref":"#/components/schemas/RuleFinding"}}},"required":["quality_score","errors","warnings","passed"]},"Severity":{"type":"string","enum":["error","warning"],"description":"How a spec that breaks the rule is reported. An error asks for a fix, and a warning points at a rule the spec would be better for meeting. A rule that names none is an error."},"Ok":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]},"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"MintedKey":{"type":"object","properties":{"$id":{"type":"string"},"org_id":{"type":"string"},"creator_id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["$id","org_id","creator_id","name"]}}}}