{
  "openapi": "3.1.0",
  "info": {
    "title": "Donatty public API",
    "version": "1.0.0",
    "summary": "Read-only data of one Donatty streamer: profile, donations, goals, top donors and totals.",
    "description": "Every route returns data of exactly the streamer who issued the key; there is no way to ask for another streamer. Authentication: `Authorization: Bearer dnt_...` only — a secret key `dnt_pat_...` (servers, bots) or the public key `dnt_pk_...` (static sites: profile, goals and statistics, accepted from the streamer's allowed site Origins only). Tokens in the query string are rejected with `400 token_in_query`; cookies are ignored.\n\nFormats: ids are `<prefix>_<uuid>` strings; time is RFC 3339 in UTC; money is `{value, currency}` with a decimal string and an ISO 4217 code. Clients must ignore unknown fields and enum values: v1 only adds.\n\nErrors are `application/problem+json` (RFC 9457) with `type`, `title`, `status`, `detail` and `code`.\n\nRate limits (per minute): `app-user` 600 (app × streamer), `app` 6000 (app over all streamers), `user` 1200 (streamer over all apps), `publishable-ip` 60 (public key × client address). `app-user`, `app` and `user` are counted separately for the secret keys and for the public key; all secret keys of a streamer share one budget. Responses after successful token authentication carry `RateLimit-Policy` and `RateLimit` (draft-ietf-httpapi-ratelimit-headers-08); responses before it (401, `wrong_token_type`, `token_in_query`, `origin_not_allowed`) and this document do not."
  },
  "servers": [
    {
      "url": "https://api.donatty.com"
    }
  ],
  "tags": [
    {
      "name": "v1",
      "description": "Public API v1"
    }
  ],
  "paths": {
    "/v1/streamer": {
      "get": {
        "operationId": "getStreamer",
        "summary": "Streamer profile",
        "description": "Public profile of the streamer who issued the token.",
        "tags": [
          "v1"
        ],
        "security": [
          {
            "personalToken": [
              "profile:read"
            ]
          },
          {
            "publishableKey": [
              "profile:read"
            ]
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Streamer"
                },
                "example": {
                  "id": "str_5b0c2f4e-8d61-4f43-9a7e-2f1d3c6b9a10",
                  "slug": "teststreamer",
                  "displayName": "Test Streamer",
                  "avatarUrl": "https://storage.donatty.com/avatars/teststreamer.png",
                  "donatePageUrl": "https://donatty.com/teststreamer"
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter, or a token passed in the query string.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-invalid-parameter": {
                    "$ref": "#/components/examples/error-invalid-parameter"
                  },
                  "error-token-in-query": {
                    "$ref": "#/components/examples/error-token-in-query"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, deleted or expired token.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-missing-token": {
                    "$ref": "#/components/examples/error-missing-token"
                  },
                  "error-invalid-token": {
                    "$ref": "#/components/examples/error-invalid-token"
                  },
                  "error-token-expired": {
                    "$ref": "#/components/examples/error-token-expired"
                  }
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "403": {
            "description": "Wrong token type, the token lacks the `profile:read` scope, or the public key is used from an Origin that is not allowed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-wrong-token-type": {
                    "$ref": "#/components/examples/error-wrong-token-type"
                  },
                  "error-insufficient-scope": {
                    "$ref": "#/components/examples/error-insufficient-scope"
                  },
                  "error-origin-not-allowed": {
                    "$ref": "#/components/examples/error-origin-not-allowed"
                  }
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "429": {
            "description": "A rate limit level is exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-rate-limited": {
                    "$ref": "#/components/examples/error-rate-limited"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-internal-error": {
                    "$ref": "#/components/examples/error-internal-error"
                  }
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unavailable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-service-unavailable": {
                    "$ref": "#/components/examples/error-service-unavailable"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            }
          }
        }
      }
    },
    "/v1/donations": {
      "get": {
        "operationId": "listDonations",
        "summary": "Donations",
        "description": "All donations of the streamer, newest first: Donatty and Donation Alerts (when connected), including ones rejected by moderation or waiting for its decision, with the full text and the current moderation status. No donor contacts, payment data or fees. Secret keys only.",
        "tags": [
          "v1"
        ],
        "security": [
          {
            "personalToken": [
              "donations:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/cursor"
          },
          {
            "$ref": "#/components/parameters/since"
          },
          {
            "$ref": "#/components/parameters/source"
          },
          {
            "$ref": "#/components/parameters/moderation"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DonationPage"
                },
                "example": {
                  "items": [
                    {
                      "id": "don_0f6e2a4c-91b3-4d7a-8c5e-3b2a1f0e9d84",
                      "createdAt": "2026-11-09T10:15:30.123456Z",
                      "source": "DONATTY",
                      "donorName": "Alex",
                      "message": "Thanks for the stream!",
                      "amount": {
                        "value": "500.00",
                        "currency": "RUB"
                      },
                      "goal": {
                        "id": "goal_7e3a9c10-4b2d-4f8e-a1c6-0d9b8e7f6a52",
                        "title": "New microphone"
                      },
                      "moderation": {
                        "status": "PENDING",
                        "deadline": "2026-11-09T10:16:30Z"
                      },
                      "media": {
                        "title": "Song of the day",
                        "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
                        "provider": "YOUTUBE",
                        "durationSeconds": 212,
                        "moderation": {
                          "status": "PENDING",
                          "deadline": "2026-11-09T10:16:30Z"
                        }
                      }
                    },
                    {
                      "id": "don_3c9d7b21-5e84-4a6f-b0d2-8e1f4a7c6b35",
                      "createdAt": "2026-11-09T10:02:11Z",
                      "source": "DONATTY",
                      "donorName": "Anonymous",
                      "message": "This text was rejected by moderation but is returned in full.",
                      "amount": {
                        "value": "150.50",
                        "currency": "RUB"
                      },
                      "goal": null,
                      "moderation": {
                        "status": "BLOCKED",
                        "deadline": null
                      },
                      "media": null
                    },
                    {
                      "id": "don_9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
                      "createdAt": "2026-11-09T09:58:00Z",
                      "source": "DONATION_ALERTS",
                      "donorName": "Maria",
                      "message": "Hi from Donation Alerts",
                      "amount": {
                        "value": "5.00",
                        "currency": "USD"
                      },
                      "goal": null,
                      "moderation": {
                        "status": "NONE",
                        "deadline": null
                      },
                      "media": null
                    }
                  ],
                  "nextCursor": "MTc5NDIxODI4MDAwMDAwMC45YTFiMmMzZC00ZTVmLTRhNmItOGM3ZC0wZTFmMmEzYjRjNWQ"
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter, or a token passed in the query string.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-invalid-parameter": {
                    "$ref": "#/components/examples/error-invalid-parameter"
                  },
                  "error-token-in-query": {
                    "$ref": "#/components/examples/error-token-in-query"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, deleted or expired token.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-missing-token": {
                    "$ref": "#/components/examples/error-missing-token"
                  },
                  "error-invalid-token": {
                    "$ref": "#/components/examples/error-invalid-token"
                  },
                  "error-token-expired": {
                    "$ref": "#/components/examples/error-token-expired"
                  }
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "403": {
            "description": "Wrong token type, the token lacks the `donations:read` scope (the public key never has it).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-wrong-token-type": {
                    "$ref": "#/components/examples/error-wrong-token-type"
                  },
                  "error-insufficient-scope": {
                    "$ref": "#/components/examples/error-insufficient-scope"
                  }
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "429": {
            "description": "A rate limit level is exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-rate-limited": {
                    "$ref": "#/components/examples/error-rate-limited"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-internal-error": {
                    "$ref": "#/components/examples/error-internal-error"
                  }
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unavailable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-service-unavailable": {
                    "$ref": "#/components/examples/error-service-unavailable"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            }
          }
        }
      }
    },
    "/v1/goals": {
      "get": {
        "operationId": "listGoals",
        "summary": "Fundraising goals",
        "description": "Donatty fundraising goals, newest first. `raised` is what the goal collected from all Donatty donations (including ones rejected by moderation) plus its manual start; Donation Alerts goals and widget settings are not counted, so the progress shown on stream may differ.",
        "tags": [
          "v1"
        ],
        "security": [
          {
            "personalToken": [
              "goals:read"
            ]
          },
          {
            "publishableKey": [
              "goals:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/goalStatus"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GoalPage"
                },
                "example": {
                  "items": [
                    {
                      "id": "goal_7e3a9c10-4b2d-4f8e-a1c6-0d9b8e7f6a52",
                      "title": "New microphone",
                      "target": {
                        "value": "15000.00",
                        "currency": "RUB"
                      },
                      "raised": {
                        "value": "6420.50",
                        "currency": "RUB"
                      },
                      "status": "ACTIVE",
                      "endsAt": "2026-12-31T21:00:00Z",
                      "createdAt": "2026-10-01T12:00:00Z"
                    }
                  ],
                  "nextCursor": "MTc5MDg1NjAwMDAwMDAwMC43ZTNhOWMxMC00YjJkLTRmOGUtYTFjNi0wZDliOGU3ZjZhNTI"
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter, or a token passed in the query string.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-invalid-parameter": {
                    "$ref": "#/components/examples/error-invalid-parameter"
                  },
                  "error-token-in-query": {
                    "$ref": "#/components/examples/error-token-in-query"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, deleted or expired token.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-missing-token": {
                    "$ref": "#/components/examples/error-missing-token"
                  },
                  "error-invalid-token": {
                    "$ref": "#/components/examples/error-invalid-token"
                  },
                  "error-token-expired": {
                    "$ref": "#/components/examples/error-token-expired"
                  }
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "403": {
            "description": "Wrong token type, the token lacks the `goals:read` scope, or the public key is used from an Origin that is not allowed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-wrong-token-type": {
                    "$ref": "#/components/examples/error-wrong-token-type"
                  },
                  "error-insufficient-scope": {
                    "$ref": "#/components/examples/error-insufficient-scope"
                  },
                  "error-origin-not-allowed": {
                    "$ref": "#/components/examples/error-origin-not-allowed"
                  }
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "429": {
            "description": "A rate limit level is exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-rate-limited": {
                    "$ref": "#/components/examples/error-rate-limited"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-internal-error": {
                    "$ref": "#/components/examples/error-internal-error"
                  }
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unavailable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-service-unavailable": {
                    "$ref": "#/components/examples/error-service-unavailable"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            }
          }
        }
      }
    },
    "/v1/stats/top": {
      "get": {
        "operationId": "getStatsTop",
        "summary": "Top donors",
        "description": "Top donors for a calendar period `[from, to)` in `timeZone`: all connected sources in RUB, donors with the same name merged across sources, sorted by total descending.",
        "tags": [
          "v1"
        ],
        "security": [
          {
            "personalToken": [
              "stats:read"
            ]
          },
          {
            "publishableKey": [
              "stats:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/period"
          },
          {
            "$ref": "#/components/parameters/topLimit"
          },
          {
            "$ref": "#/components/parameters/timeZone"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Top"
                },
                "example": {
                  "period": "WEEK",
                  "from": "2026-11-08T21:00:00Z",
                  "to": "2026-11-15T21:00:00Z",
                  "items": [
                    {
                      "rank": 1,
                      "name": "Alex",
                      "total": {
                        "value": "12500.00",
                        "currency": "RUB"
                      }
                    },
                    {
                      "rank": 2,
                      "name": "Maria",
                      "total": {
                        "value": "4300.75",
                        "currency": "RUB"
                      }
                    },
                    {
                      "rank": 3,
                      "name": "Ivan",
                      "total": {
                        "value": "1000.00",
                        "currency": "RUB"
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter, or a token passed in the query string.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-invalid-parameter": {
                    "$ref": "#/components/examples/error-invalid-parameter"
                  },
                  "error-token-in-query": {
                    "$ref": "#/components/examples/error-token-in-query"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, deleted or expired token.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-missing-token": {
                    "$ref": "#/components/examples/error-missing-token"
                  },
                  "error-invalid-token": {
                    "$ref": "#/components/examples/error-invalid-token"
                  },
                  "error-token-expired": {
                    "$ref": "#/components/examples/error-token-expired"
                  }
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "403": {
            "description": "Wrong token type, the token lacks the `stats:read` scope, or the public key is used from an Origin that is not allowed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-wrong-token-type": {
                    "$ref": "#/components/examples/error-wrong-token-type"
                  },
                  "error-insufficient-scope": {
                    "$ref": "#/components/examples/error-insufficient-scope"
                  },
                  "error-origin-not-allowed": {
                    "$ref": "#/components/examples/error-origin-not-allowed"
                  }
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "429": {
            "description": "A rate limit level is exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-rate-limited": {
                    "$ref": "#/components/examples/error-rate-limited"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-internal-error": {
                    "$ref": "#/components/examples/error-internal-error"
                  }
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unavailable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-service-unavailable": {
                    "$ref": "#/components/examples/error-service-unavailable"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            }
          }
        }
      }
    },
    "/v1/stats/total": {
      "get": {
        "operationId": "getStatsTotal",
        "summary": "Total donated",
        "description": "Sum of donations for a calendar period `[from, to)` in `timeZone`: all connected sources in RUB.",
        "tags": [
          "v1"
        ],
        "security": [
          {
            "personalToken": [
              "stats:read"
            ]
          },
          {
            "publishableKey": [
              "stats:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/period"
          },
          {
            "$ref": "#/components/parameters/timeZone"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Total"
                },
                "example": {
                  "period": "DAY",
                  "from": "2026-11-09T17:00:00Z",
                  "to": "2026-11-10T17:00:00Z",
                  "total": {
                    "value": "18250.25",
                    "currency": "RUB"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter, or a token passed in the query string.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-invalid-parameter": {
                    "$ref": "#/components/examples/error-invalid-parameter"
                  },
                  "error-token-in-query": {
                    "$ref": "#/components/examples/error-token-in-query"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, deleted or expired token.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-missing-token": {
                    "$ref": "#/components/examples/error-missing-token"
                  },
                  "error-invalid-token": {
                    "$ref": "#/components/examples/error-invalid-token"
                  },
                  "error-token-expired": {
                    "$ref": "#/components/examples/error-token-expired"
                  }
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "403": {
            "description": "Wrong token type, the token lacks the `stats:read` scope, or the public key is used from an Origin that is not allowed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-wrong-token-type": {
                    "$ref": "#/components/examples/error-wrong-token-type"
                  },
                  "error-insufficient-scope": {
                    "$ref": "#/components/examples/error-insufficient-scope"
                  },
                  "error-origin-not-allowed": {
                    "$ref": "#/components/examples/error-origin-not-allowed"
                  }
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "429": {
            "description": "A rate limit level is exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-rate-limited": {
                    "$ref": "#/components/examples/error-rate-limited"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-internal-error": {
                    "$ref": "#/components/examples/error-internal-error"
                  }
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unavailable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "error-service-unavailable": {
                    "$ref": "#/components/examples/error-service-unavailable"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "personalToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "dnt_pat_...",
        "description": "Secret key: chosen scopes, 30/90/365 days, shown once. For servers and bots; keep it secret."
      },
      "publishableKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "dnt_pk_...",
        "description": "Public key: profile:read, goals:read, stats:read; accepted only with an `Origin` header of the streamer's allowed sites. Visible to site visitors."
      }
    },
    "parameters": {
      "limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      },
      "topLimit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 10
        }
      },
      "cursor": {
        "name": "cursor",
        "in": "query",
        "description": "`nextCursor` of the previous page.",
        "schema": {
          "type": "string"
        }
      },
      "since": {
        "name": "since",
        "in": "query",
        "description": "Only donations created at or after this moment (RFC 3339).",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "source": {
        "name": "source",
        "in": "query",
        "description": "Comma-separated sources.",
        "style": "form",
        "explode": false,
        "schema": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "DONATTY",
              "DONATION_ALERTS"
            ]
          }
        }
      },
      "moderation": {
        "name": "moderation",
        "in": "query",
        "description": "Comma-separated moderation statuses, e.g. `PASSED,NONE`.",
        "style": "form",
        "explode": false,
        "schema": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "PENDING",
              "PASSED",
              "BLOCKED",
              "NONE"
            ]
          }
        }
      },
      "goalStatus": {
        "name": "status",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "ACTIVE",
            "CLOSED"
          ]
        }
      },
      "period": {
        "name": "period",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "DAY",
            "WEEK",
            "MONTH",
            "YEAR",
            "ALL_TIME"
          ],
          "default": "ALL_TIME"
        }
      },
      "timeZone": {
        "name": "timeZone",
        "in": "query",
        "description": "IANA time zone of the calendar period.",
        "schema": {
          "type": "string",
          "default": "Europe/Moscow",
          "examples": [
            "Asia/Novosibirsk"
          ]
        }
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "Active levels: `\"<level>\";q=<quota>;w=<window seconds>`.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit": {
        "description": "Remaining per level: `\"<level>\";r=<remaining>;t=<seconds to reset>`.",
        "schema": {
          "type": "string"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait.",
        "schema": {
          "type": "integer"
        }
      },
      "WWW-Authenticate": {
        "description": "`Bearer resource_metadata=\"...\"` on 401; `Bearer error=\"insufficient_scope\", scope=\"...\"` on 403.",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "Money": {
        "type": "object",
        "required": [
          "value",
          "currency"
        ],
        "additionalProperties": false,
        "properties": {
          "value": {
            "type": "string",
            "pattern": "^-?\\d+\\.\\d{2}$",
            "description": "Decimal amount with two fraction digits.",
            "examples": [
              "500.00"
            ]
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code.",
            "examples": [
              "RUB"
            ]
          }
        }
      },
      "Moderation": {
        "type": "object",
        "required": [
          "status",
          "deadline"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "PASSED",
              "BLOCKED",
              "NONE"
            ],
            "description": "`PENDING` — moderation in progress; `PASSED` — approved (including «Repeat on stream»); `BLOCKED` — rejected; `NONE` — no moderation (disabled, nobody online, Donation Alerts)."
          },
          "deadline": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Only for `PENDING`: when the default decision applies."
          }
        }
      },
      "Streamer": {
        "type": "object",
        "required": [
          "id",
          "slug",
          "displayName",
          "avatarUrl",
          "donatePageUrl"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^str_[0-9a-f-]{36}$"
          },
          "slug": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "avatarUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "donatePageUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Donation": {
        "type": "object",
        "required": [
          "id",
          "createdAt",
          "source",
          "donorName",
          "message",
          "amount",
          "goal",
          "moderation",
          "media"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^don_[0-9a-f-]{36}$"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "source": {
            "type": "string",
            "enum": [
              "DONATTY",
              "DONATION_ALERTS"
            ]
          },
          "donorName": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "amount": {
            "$ref": "#/components/schemas/Money"
          },
          "goal": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": [
                  "id",
                  "title"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "pattern": "^goal_[0-9a-f-]{36}$"
                  },
                  "title": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "moderation": {
            "$ref": "#/components/schemas/Moderation"
          },
          "media": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": [
                  "title",
                  "url",
                  "provider",
                  "durationSeconds",
                  "moderation"
                ],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "provider": {
                    "type": "string",
                    "description": "YOUTUBE, VK, RUTUBE, TWITCH; new values may appear."
                  },
                  "durationSeconds": {
                    "type": "integer"
                  },
                  "moderation": {
                    "$ref": "#/components/schemas/Moderation"
                  }
                }
              }
            ]
          }
        }
      },
      "DonationPage": {
        "type": "object",
        "required": [
          "items",
          "nextCursor"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Donation"
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` to get the next page; `null` — last page."
          }
        }
      },
      "Goal": {
        "type": "object",
        "required": [
          "id",
          "title",
          "target",
          "raised",
          "status",
          "endsAt",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^goal_[0-9a-f-]{36}$"
          },
          "title": {
            "type": "string"
          },
          "target": {
            "$ref": "#/components/schemas/Money"
          },
          "raised": {
            "$ref": "#/components/schemas/Money"
          },
          "status": {
            "type": "string",
            "enum": [
              "ACTIVE",
              "CLOSED"
            ]
          },
          "endsAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "GoalPage": {
        "type": "object",
        "required": [
          "items",
          "nextCursor"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Goal"
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` to get the next page; `null` — last page."
          }
        }
      },
      "Top": {
        "type": "object",
        "required": [
          "period",
          "from",
          "to",
          "items"
        ],
        "properties": {
          "period": {
            "type": "string",
            "enum": [
              "DAY",
              "WEEK",
              "MONTH",
              "YEAR",
              "ALL_TIME"
            ]
          },
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time",
            "description": "Exclusive: start of the next period; for ALL_TIME — the response moment."
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "rank",
                "name",
                "total"
              ],
              "properties": {
                "rank": {
                  "type": "integer",
                  "minimum": 1
                },
                "name": {
                  "type": "string"
                },
                "total": {
                  "$ref": "#/components/schemas/Money"
                }
              }
            }
          }
        }
      },
      "Total": {
        "type": "object",
        "required": [
          "period",
          "from",
          "to",
          "total"
        ],
        "properties": {
          "period": {
            "type": "string",
            "enum": [
              "DAY",
              "WEEK",
              "MONTH",
              "YEAR",
              "ALL_TIME"
            ]
          },
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time",
            "description": "Exclusive."
          },
          "total": {
            "$ref": "#/components/schemas/Money"
          }
        }
      },
      "Problem": {
        "type": "object",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "https://developers.donatty.com/errors/<code>"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "missing_token",
              "invalid_token",
              "token_expired",
              "token_in_query",
              "wrong_token_type",
              "insufficient_scope",
              "origin_not_allowed",
              "invalid_parameter",
              "not_found",
              "rate_limited",
              "internal_error",
              "service_unavailable"
            ]
          },
          "policy": {
            "type": "string",
            "enum": [
              "app-user",
              "app",
              "user",
              "publishable-ip"
            ],
            "description": "Only for `rate_limited`: the exhausted level."
          }
        }
      }
    },
    "examples": {
      "error-missing-token": {
        "value": {
          "type": "https://developers.donatty.com/errors/missing_token",
          "title": "Missing token",
          "status": 401,
          "detail": "Send the app token in the Authorization header: Bearer dnt_...",
          "code": "missing_token"
        }
      },
      "error-invalid-token": {
        "value": {
          "type": "https://developers.donatty.com/errors/invalid_token",
          "title": "Invalid token",
          "status": 401,
          "detail": "The token is unknown, deleted or malformed.",
          "code": "invalid_token"
        }
      },
      "error-token-expired": {
        "value": {
          "type": "https://developers.donatty.com/errors/token_expired",
          "title": "Token expired",
          "status": 401,
          "detail": "The token has expired. Issue a new one in the Donatty dashboard.",
          "code": "token_expired"
        }
      },
      "error-token-in-query": {
        "value": {
          "type": "https://developers.donatty.com/errors/token_in_query",
          "title": "Token in query",
          "status": 400,
          "detail": "Tokens are accepted only in the Authorization header. The token from the query string was not used.",
          "code": "token_in_query"
        }
      },
      "error-wrong-token-type": {
        "value": {
          "type": "https://developers.donatty.com/errors/wrong_token_type",
          "title": "Wrong token type",
          "status": 403,
          "detail": "Public API routes accept only Donatty app tokens (dnt_...); other routes never accept them.",
          "code": "wrong_token_type"
        }
      },
      "error-insufficient-scope": {
        "value": {
          "type": "https://developers.donatty.com/errors/insufficient_scope",
          "title": "Insufficient scope",
          "status": 403,
          "detail": "This route requires the donations:read scope.",
          "code": "insufficient_scope"
        }
      },
      "error-origin-not-allowed": {
        "value": {
          "type": "https://developers.donatty.com/errors/origin_not_allowed",
          "title": "Origin not allowed",
          "status": 403,
          "detail": "This publishable key accepts requests only from the origins allowed for its app.",
          "code": "origin_not_allowed"
        }
      },
      "error-invalid-parameter": {
        "value": {
          "type": "https://developers.donatty.com/errors/invalid_parameter",
          "title": "Invalid parameter",
          "status": 400,
          "detail": "The limit parameter must be an integer from 1 to 100.",
          "code": "invalid_parameter"
        }
      },
      "error-not-found": {
        "value": {
          "type": "https://developers.donatty.com/errors/not_found",
          "title": "Not found",
          "status": 404,
          "detail": "There is no such public API route.",
          "code": "not_found"
        }
      },
      "error-rate-limited": {
        "value": {
          "type": "https://developers.donatty.com/errors/rate_limited",
          "title": "Rate limited",
          "status": 429,
          "detail": "The app-user rate limit is exhausted.",
          "code": "rate_limited",
          "policy": "app-user"
        }
      },
      "error-internal-error": {
        "value": {
          "type": "https://developers.donatty.com/errors/internal_error",
          "title": "Internal error",
          "status": 500,
          "detail": "The request failed on the server. Try again later.",
          "code": "internal_error"
        }
      },
      "error-service-unavailable": {
        "value": {
          "type": "https://developers.donatty.com/errors/service_unavailable",
          "title": "Service unavailable",
          "status": 503,
          "detail": "The API is temporarily unavailable. Retry after the delay in Retry-After.",
          "code": "service_unavailable"
        }
      }
    }
  }
}
