{
  "openapi": "3.1.0",
  "info": {
    "title": "uhm.co API",
    "version": "1.0.0",
    "description": "Create and manage short links from other software. Server-side only: an API key must never be placed in browser JavaScript, and no CORS headers are ever sent, so a browser cannot call this API even if a key were embedded in one."
  },
  "servers": [{ "url": "https://api.uhm.co" }],
  "security": [{ "bearerAuth": [] }],
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "uhm_live_" }
    },
    "schemas": {
      "Link": {
        "type": "object",
        "required": [
          "id", "slug", "short_url", "destination_url", "title",
          "reference", "is_active", "expires_at", "created_at"
        ],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "slug": { "type": "string" },
          "short_url": { "type": "string", "format": "uri" },
          "destination_url": { "type": "string", "format": "uri" },
          "title": { "type": ["string", "null"] },
          "reference": { "type": ["string", "null"] },
          "is_active": { "type": "boolean" },
          "expires_at": { "type": ["string", "null"], "format": "date-time" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "LinkPage": {
        "type": "object",
        "required": ["data", "next_cursor"],
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } },
          "next_cursor": {
            "type": ["string", "null"],
            "description": "Pass as ?cursor= to fetch the next page. Null on the last page."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "docs_url"],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request", "invalid_slug", "destination_rejected", "unauthenticated",
                  "insufficient_scope", "feature_not_available", "api_not_included",
                  "workspace_banned", "trial_expired", "domain_not_verified", "not_found",
                  "quota_exceeded", "slug_taken", "slug_reserved", "destination_unsafe",
                  "rate_limited", "internal_error", "api_not_configured"
                ]
              },
              "message": {
                "type": "string",
                "description": "A developer-facing debug string, English only regardless of the calling workspace's locale. Branch on code, never on this."
              },
              "docs_url": {
                "type": "string",
                "format": "uri",
                "description": "https://uhm.co/en/developers#{code} -- always the English documentation page, for the same reason message is English-only."
              }
            }
          }
        }
      },
      "Stats": {
        "type": "object",
        "required": ["range", "totals", "bot_reasons", "series", "breakdowns"],
        "properties": {
          "range": {
            "type": "object",
            "properties": { "days": { "type": "integer" } }
          },
          "totals": {
            "type": "object",
            "properties": {
              "clicks": { "type": "integer" },
              "humans": { "type": "integer" },
              "bots": { "type": "integer" }
            }
          },
          "bot_reasons": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "reason": {
                  "type": "string",
                  "enum": ["known_crawler", "prefetch", "datacenter", "no_user_agent"]
                },
                "count": { "type": "integer" }
              }
            }
          },
          "series": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": { "type": "string", "format": "date" },
                "humans": { "type": "integer" },
                "bots": { "type": "integer" }
              }
            }
          },
          "breakdowns": {
            "type": "object",
            "properties": {
              "country": { "$ref": "#/components/schemas/BreakdownRows" },
              "device": { "$ref": "#/components/schemas/BreakdownRows" },
              "referrer": { "$ref": "#/components/schemas/BreakdownRows" },
              "source": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "source": { "type": "string", "enum": ["direct", "qr"] },
                    "clicks": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      },
      "BreakdownRows": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "key": { "type": "string" },
            "clicks": { "type": "integer" }
          }
        }
      },
      "Me": {
        "type": "object",
        "required": ["workspace", "usage", "key"],
        "properties": {
          "workspace": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "plan": { "type": "string" },
              "plan_status": { "type": "string" },
              "currency": { "type": "string" }
            }
          },
          "usage": {
            "type": ["object", "null"],
            "description": "Null when the workspace has not had a usage period materialise yet.",
            "properties": {
              "links_created": { "type": "integer" },
              "links_included": { "type": "integer" },
              "links_remaining": { "type": "integer" },
              "period_start": { "type": "string", "format": "date" },
              "period_end": { "type": "string", "format": "date" }
            }
          },
          "key": {
            "type": "object",
            "properties": {
              "scopes": {
                "type": "array",
                "items": { "type": "string", "enum": ["links:read", "links:write", "analytics:read"] }
              }
            }
          }
        }
      }
    },
    "responses": {
      "Unauthenticated": {
        "description": "Missing, unknown, revoked or expired key.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "insufficient_scope, api_not_included, feature_not_available, workspace_banned, trial_expired or domain_not_verified.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "No such link, or a link belonging to another workspace.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Too many requests in the current window. Retry-After is set.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "QuotaExceeded": {
        "description": "The monthly link allowance is spent and no credit remains. Existing links keep redirecting.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ServiceUnavailable": {
        "description": "api_not_configured -- the API is not configured on this deployment. Checked before authentication, so it can precede every other response on this list.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "InternalError": {
        "description": "internal_error -- an unexpected failure on our side, not a rejection of the request. Every handler is wrapped so this can surface from any operation; safe to retry.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  },
  "paths": {
    "/v1/me": {
      "get": {
        "summary": "Describe the key, its workspace, and the remaining allowance",
        "description": "No scope is required. A key that cannot introspect itself makes a misconfiguration impossible to diagnose from an integration's own settings screen.",
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/v1/links": {
      "get": {
        "summary": "List links",
        "description": "Requires the links:read scope.",
        "parameters": [
          { "name": "page_size", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } },
          { "name": "cursor", "in": "query", "schema": { "type": "string" }, "description": "A next_cursor value from a previous page." },
          { "name": "reference", "in": "query", "schema": { "type": "string" }, "description": "Restrict the page to links carrying this reference." }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LinkPage" } } }
          },
          "400": {
            "description": "invalid_request",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      },
      "post": {
        "summary": "Create a link, or return the existing link for a reference",
        "description": "Requires the links:write scope. Sending a reference that already exists in this workspace returns the stored link with status 200 and consumes no allowance. A genuine create returns 201.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["destination_url"],
                "properties": {
                  "destination_url": { "type": "string", "format": "uri" },
                  "reference": { "type": "string", "maxLength": 255 },
                  "slug": { "type": "string" },
                  "title": { "type": ["string", "null"] },
                  "expires_at": { "type": ["string", "null"], "format": "date-time" },
                  "password": { "type": ["string", "null"] },
                  "domain_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "One of the workspace's own verified domains. Omitted for the root uhm.co namespace."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Link" } } }
          },
          "200": {
            "description": "An existing reference matched; nothing was created.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Link" } } }
          },
          "400": {
            "description": "invalid_request, invalid_slug or destination_rejected",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": {
            "description": "not_found -- domain_id does not name one of this workspace's own domains.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "409": {
            "description": "slug_taken or slug_reserved",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "422": {
            "description": "destination_unsafe",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/v1/links/{id}": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
      ],
      "get": {
        "summary": "Read one link",
        "description": "Requires the links:read scope.",
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Link" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      },
      "patch": {
        "summary": "Update a link",
        "description": "Requires the links:write scope. The reference cannot be changed by this call. It is how an integration recognises a link it already made, so a PATCH that could move it would let one call silently orphan another.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "destination_url": { "type": "string", "format": "uri" },
                  "slug": { "type": "string" },
                  "title": { "type": ["string", "null"] },
                  "is_active": { "type": "boolean" },
                  "expires_at": { "type": ["string", "null"], "format": "date-time" },
                  "password": { "type": ["string", "null"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Link" } } }
          },
          "400": {
            "description": "invalid_request or invalid_slug",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "slug_taken or slug_reserved",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "422": {
            "description": "destination_unsafe",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      },
      "delete": {
        "summary": "Delete a link",
        "description": "Requires the links:write scope. The slug stops resolving immediately. Analytics for it are removed with the link.",
        "responses": {
          "204": { "description": "Deleted" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/v1/links/{id}/stats": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
        {
          "name": "days",
          "in": "query",
          "schema": { "type": "integer", "default": 30, "minimum": 1, "maximum": 1095 },
          "description": "The trailing window, in whole days."
        }
      ],
      "get": {
        "summary": "Click statistics for one link",
        "description": "Requires the analytics:read scope. The same numbers the dashboard shows, from the same source. Bot traffic is reported separately and never folded into the human count; see https://uhm.co/en/methodology for what counts as a bot.",
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Stats" } } }
          },
          "400": {
            "description": "invalid_request",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    }
  }
}
