{
  "openapi": "3.1.0",
  "info": {
    "title": "hejj app widgets API",
    "version": "1",
    "description": "How apps connect to hejj and send widget data. The full guide, with flows and examples: https://hejj.net/developers/widgets.md"
  },
  "servers": [{ "url": "https://hejj.net" }],
  "components": { "securitySchemes": { "bearer": { "type": "http", "scheme": "bearer" } } },
  "paths": {
    "/api/apps/v1/token": {
      "post": {
        "summary": "Exchange an authorization code (PKCE) or poll a device code",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "required": [
                      "grant_type",
                      "code",
                      "code_verifier",
                      "client_id",
                      "redirect_uri"
                    ],
                    "properties": {
                      "grant_type": { "const": "authorization_code" },
                      "code": { "type": "string" },
                      "code_verifier": { "type": "string" },
                      "client_id": { "type": "string", "format": "uri" },
                      "redirect_uri": { "type": "string", "format": "uri" }
                    }
                  },
                  {
                    "type": "object",
                    "required": ["grant_type", "device_code"],
                    "properties": {
                      "grant_type": { "const": "urn:ietf:params:oauth:grant-type:device_code" },
                      "device_code": { "type": "string" }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The grant's credentials",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "access_token",
                    "token_type",
                    "signing_secret",
                    "grant_id",
                    "widget",
                    "endpoints",
                    "context"
                  ],
                  "properties": {
                    "access_token": { "type": "string", "pattern": "^hejj_wt_[A-Za-z0-9_-]{43}$" },
                    "token_type": { "const": "Bearer" },
                    "signing_secret": {
                      "type": "string",
                      "pattern": "^hejj_ws_[A-Za-z0-9_-]{43}$"
                    },
                    "grant_id": { "type": "string", "pattern": "^gr_[A-Za-z0-9_-]{22}$" },
                    "widget": { "type": "string" },
                    "endpoints": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "mode": { "enum": ["pull", "push"] }
                        }
                      }
                    },
                    "context": { "type": "array", "items": { "enum": ["timezone", "locale"] } }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_request, invalid_grant, invalid_client, unsupported_grant_type, access_denied, authorization_pending, slow_down, expired_token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["error"],
                  "properties": {
                    "error": { "type": "string" },
                    "error_description": { "type": "string" }
                  }
                }
              }
            }
          },
          "429": { "description": "Too many requests (Retry-After)" }
        }
      }
    },
    "/api/apps/v1/device": {
      "post": {
        "summary": "Start a device-code flow (RFC 8628)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["widget"],
                "properties": {
                  "client_id": {
                    "type": "string",
                    "format": "uri",
                    "description": "Hosted manifest URL"
                  },
                  "client_secret": {
                    "type": "string",
                    "description": "With client_id: sha256 must equal the manifest's device_secret_sha256"
                  },
                  "manifest": {
                    "$ref": "https://hejj.net/schemas/manifest-1.json",
                    "description": "Inline manifest (unverified, push only)"
                  },
                  "widget": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Show user_code and verification_uri to the user, then poll /token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "device_code": { "type": "string" },
                    "user_code": { "type": "string", "pattern": "^[A-Z]{4}-[A-Z]{4}$" },
                    "verification_uri": { "type": "string", "format": "uri" },
                    "verification_uri_complete": { "type": "string", "format": "uri" },
                    "interval": { "type": "integer" },
                    "expires_in": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_request or invalid_client",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["error"],
                  "properties": {
                    "error": { "type": "string" },
                    "error_description": { "type": "string" }
                  }
                }
              }
            }
          },
          "401": {
            "description": "client_secret doesn't match",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["error"],
                  "properties": {
                    "error": { "type": "string" },
                    "error_description": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/apps/v1/push/{endpoint}": {
      "post": {
        "summary": "Replace a push endpoint's data",
        "security": [{ "bearer": [] }],
        "parameters": [
          { "name": "endpoint", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "content": {
            "application/json": { "schema": { "$ref": "https://hejj.net/schemas/blocks-1.json" } }
          }
        },
        "responses": {
          "200": {
            "description": "Stored; open tabs update",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "skipped": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": { "type": "integer" },
                          "type": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Token revoked or unknown",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string" },
                        "message": { "type": "string" },
                        "issues": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "path": { "type": "string" },
                              "message": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not one of this grant's push endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string" },
                        "message": { "type": "string" },
                        "issues": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "path": { "type": "string" },
                              "message": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "Over 64 KB",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string" },
                        "message": { "type": "string" },
                        "issues": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "path": { "type": "string" },
                              "message": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Invalid blocks: every issue with its path",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string" },
                        "message": { "type": "string" },
                        "issues": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "path": { "type": "string" },
                              "message": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "60/min or 1 000/day exceeded (Retry-After)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string" },
                        "message": { "type": "string" },
                        "issues": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "path": { "type": "string" },
                              "message": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/apps/v1/grant": {
      "get": {
        "summary": "What this token may do",
        "security": [{ "bearer": [] }],
        "responses": {
          "200": {
            "description": "The grant",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "grant_id": { "type": "string" },
                    "widget": { "type": "string" },
                    "endpoints": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "mode": { "enum": ["pull", "push"] }
                        }
                      }
                    },
                    "context": { "type": "array", "items": { "enum": ["timezone", "locale"] } }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Token revoked or unknown",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string" },
                        "message": { "type": "string" },
                        "issues": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "path": { "type": "string" },
                              "message": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Uninstall: the token stops working",
        "security": [{ "bearer": [] }],
        "responses": {
          "204": { "description": "Gone" },
          "401": {
            "description": "Unknown",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string" },
                        "message": { "type": "string" },
                        "issues": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "path": { "type": "string" },
                              "message": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/apps/v1/token/rotate": {
      "post": {
        "summary": "New token and signing secret; the old token works 24 h more",
        "security": [{ "bearer": [] }],
        "responses": {
          "200": {
            "description": "New credentials",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "access_token",
                    "token_type",
                    "signing_secret",
                    "grant_id",
                    "widget",
                    "endpoints",
                    "context"
                  ],
                  "properties": {
                    "access_token": { "type": "string", "pattern": "^hejj_wt_[A-Za-z0-9_-]{43}$" },
                    "token_type": { "const": "Bearer" },
                    "signing_secret": {
                      "type": "string",
                      "pattern": "^hejj_ws_[A-Za-z0-9_-]{43}$"
                    },
                    "grant_id": { "type": "string", "pattern": "^gr_[A-Za-z0-9_-]{22}$" },
                    "widget": { "type": "string" },
                    "endpoints": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "mode": { "enum": ["pull", "push"] }
                        }
                      }
                    },
                    "context": { "type": "array", "items": { "enum": ["timezone", "locale"] } }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unknown",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string" },
                        "message": { "type": "string" },
                        "issues": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "path": { "type": "string" },
                              "message": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "pull": {
      "get": {
        "summary": "hejj fetches a pull endpoint (your manifest's url) while a tile shows it",
        "parameters": [
          { "name": "grant", "in": "query", "required": true, "schema": { "type": "string" } },
          {
            "name": "tz",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Only if granted"
          },
          {
            "name": "locale",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Only if granted"
          },
          {
            "name": "Hejj-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix>,v1=<hex HMAC-SHA256(signing_secret, \"<t>.<METHOD>.<path?query>.<hex sha256(body)>\")>. Reject if |now - t| > 300.",
            "schema": { "type": "string" }
          },
          { "name": "Hejj-Grant", "in": "header", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "The tile's blocks",
            "content": {
              "application/json": { "schema": { "$ref": "https://hejj.net/schemas/blocks-1.json" } }
            }
          },
          "404": { "description": "Unknown grant: the tile asks the user to reconnect" }
        }
      }
    },
    "action": {
      "post": {
        "summary": "hejj calls your actions_url when a button is tapped",
        "parameters": [
          {
            "name": "Hejj-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix>,v1=<hex HMAC-SHA256(signing_secret, \"<t>.<METHOD>.<path?query>.<hex sha256(body)>\")>. Reject if |now - t| > 300.",
            "schema": { "type": "string" }
          },
          { "name": "Hejj-Grant", "in": "header", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["grant_id", "endpoint", "action"],
                "properties": {
                  "grant_id": { "type": "string" },
                  "endpoint": { "type": "string" },
                  "action": { "type": "string" },
                  "button_value": { "type": "string" },
                  "tz": { "type": "string" },
                  "locale": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New blocks, or a toast",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "https://hejj.net/schemas/blocks-1.json" },
                    { "type": "object", "properties": { "toast": { "type": "string" } } }
                  ]
                }
              }
            }
          },
          "204": { "description": "Done, nothing to show" }
        }
      }
    }
  }
}
