diff --git a/SUMMARY.md b/SUMMARY.md index 5d5c159..0390d0b 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -27,3 +27,8 @@ * [Moves & Movesets](battling/moves-and-movesets.md) * [Battles](battling/battles.md) + +## API + +* [Authentication](api/authentication.md) +* [Rate Limiting](api/rate-limiting.md) diff --git a/api/authentication.md b/api/authentication.md new file mode 100644 index 0000000..b9e13c0 --- /dev/null +++ b/api/authentication.md @@ -0,0 +1,301 @@ +--- +description: >- + Learn how to authenticate with the Pokétwo API using API keys and OAuth, + and follow security best practices to keep your credentials safe. +--- + +# Authentication + +{% hint style="warning" %} +This site is an early **work in progress**. Many pages may be missing or incomplete. Please let us know at [discord.gg/poketwo](https://discord.gg/poketwo) if you would like to help write or improve a page. +{% endhint %} + +The Pokétwo API supports two authentication methods: **API key authentication** for server-to-server integrations and **OAuth 2.0** for applications acting on behalf of a user. + +## API Key Authentication + +API keys provide a simple way to authenticate requests. Each key is **scoped per-bot** — meaning every bot registered under your account has its own independent set of API keys. Keys created for one bot cannot be used to access resources belonging to another bot. + +### Obtaining an API Key + +1. Log in to the [Pokétwo Developer Portal](https://poketwo.net/developers). +2. Navigate to **Settings > API Keys**. +3. Click **Create New Key**. +4. Give your key a descriptive name and select the permission scopes it needs. +5. Copy the key immediately — it will only be displayed once. + +{% hint style="danger" %} +Treat your API key like a password. Never share it publicly or commit it to version control. If you believe a key has been compromised, revoke it immediately and create a new one. +{% endhint %} + +### Using Your API Key + +Include the API key in the `Authorization` header of every request using the `Bearer` scheme. + +```bash +curl -H "Authorization: Bearer YOUR_API_KEY" \ + https://api.poketwo.net/v1/pokemon +``` + +#### Python + +```python +import requests + +headers = {"Authorization": "Bearer YOUR_API_KEY"} +response = requests.get("https://api.poketwo.net/v1/pokemon", headers=headers) +print(response.json()) +``` + +#### JavaScript + +```javascript +const response = await fetch("https://api.poketwo.net/v1/pokemon", { + headers: { Authorization: "Bearer YOUR_API_KEY" }, +}); +const data = await response.json(); +console.log(data); +``` + +### API Key Scopes + +When creating an API key, you can restrict its access to specific scopes. + +| Scope | Description | +| ------------------ | -------------------------------------------- | +| `pokemon:read` | View Pokémon data (species, stats, etc.). | +| `pokemon:write` | Modify Pokémon data (nicknames, etc.). | +| `market:read` | View market listings. | +| `market:write` | Create and manage market listings. | +| `user:read` | View user profile information. | +| `user:write` | Update user settings. | +| `trade:read` | View trade history. | +| `trade:write` | Initiate and manage trades. | + +{% hint style="info" %} +Always follow the principle of least privilege — only grant your key the scopes it actually needs. +{% endhint %} + +## OAuth 2.0 + +OAuth 2.0 allows third-party applications to access the API on behalf of a user without exposing their credentials. Pokétwo implements the **Authorization Code** flow. + +### Registering Your Application + +1. Go to the [Pokétwo Developer Portal](https://poketwo.net/developers). +2. Navigate to **Applications** and click **New Application**. +3. Fill in your application name, description, and **Redirect URI**. +4. Note your **Client ID** and **Client Secret**. + +### Authorization Code Flow + +The OAuth 2.0 Authorization Code flow consists of four steps: + +#### Step 1: Redirect the User to Authorize + +Direct the user's browser to the authorization endpoint. + +``` +GET https://poketwo.net/oauth/authorize + ?client_id=YOUR_CLIENT_ID + &redirect_uri=https://yourapp.com/callback + &response_type=code + &scope=pokemon:read user:read + &state=RANDOM_STATE_STRING +``` + +| Parameter | Description | +| --------------- | ------------------------------------------------------------------- | +| `client_id` | Your application's Client ID. | +| `redirect_uri` | The URI to redirect to after authorization (must match registered). | +| `response_type` | Must be `code`. | +| `scope` | Space-separated list of requested scopes. | +| `state` | A random string to prevent CSRF attacks (recommended). | + +#### Step 2: User Grants Permission + +The user reviews the requested permissions and clicks **Authorize**. Pokétwo redirects back to your `redirect_uri` with an authorization code. + +``` +https://yourapp.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATE_STRING +``` + +#### Step 3: Exchange the Code for Tokens + +Make a server-side POST request to exchange the authorization code for an access token. + +```bash +curl -X POST https://poketwo.net/oauth/token \ + -d "grant_type=authorization_code" \ + -d "code=AUTHORIZATION_CODE" \ + -d "redirect_uri=https://yourapp.com/callback" \ + -d "client_id=YOUR_CLIENT_ID" \ + -d "client_secret=YOUR_CLIENT_SECRET" +``` + +The response contains an access token and a refresh token: + +```json +{ + "access_token": "eyJhbGciOi...", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "dGhpcyBpcyBh...", + "scope": "pokemon:read user:read" +} +``` + +#### Step 4: Use the Access Token + +Include the access token in the `Authorization` header, just like an API key. + +```bash +curl -H "Authorization: Bearer ACCESS_TOKEN" \ + https://api.poketwo.net/v1/user/me +``` + +### Refreshing Tokens + +Access tokens expire after the duration indicated by `expires_in`. Use the refresh token to obtain a new access token without requiring the user to re-authorize. + +```bash +curl -X POST https://poketwo.net/oauth/token \ + -d "grant_type=refresh_token" \ + -d "refresh_token=YOUR_REFRESH_TOKEN" \ + -d "client_id=YOUR_CLIENT_ID" \ + -d "client_secret=YOUR_CLIENT_SECRET" +``` + +```python +import requests + +response = requests.post( + "https://poketwo.net/oauth/token", + data={ + "grant_type": "refresh_token", + "refresh_token": "YOUR_REFRESH_TOKEN", + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET", + }, +) + +tokens = response.json() +new_access_token = tokens["access_token"] +``` + +### Revoking Tokens + +To revoke an access or refresh token (e.g. when a user logs out), send a POST request to the revocation endpoint. + +```bash +curl -X POST https://poketwo.net/oauth/revoke \ + -d "token=TOKEN_TO_REVOKE" \ + -d "client_id=YOUR_CLIENT_ID" \ + -d "client_secret=YOUR_CLIENT_SECRET" +``` + +A successful revocation returns an HTTP 200 with an empty body. Revoking a refresh token also invalidates any access tokens issued from it. + +### OAuth Endpoints Reference + +| Endpoint | Method | Description | +| ------------------------------------- | ------ | ------------------------------------------------ | +| `https://poketwo.net/oauth/authorize` | GET | Authorization page — redirect users here. | +| `https://poketwo.net/oauth/token` | POST | Exchange auth codes, refresh tokens. | +| `https://poketwo.net/oauth/revoke` | POST | Revoke an access or refresh token. | +| `https://api.poketwo.net/v1/user/me` | GET | Fetch the authenticated user's profile. | + +## Security Best Practices + +1. **Never expose secrets client-side.** API keys, client secrets, and refresh tokens must only be used in server-side code. Never embed them in frontend JavaScript, mobile apps, or public repositories. + +2. **Use environment variables.** Store credentials in environment variables or a secrets manager rather than hard-coding them in your source code. + + ```bash + export POKETWO_API_KEY="your-api-key-here" + ``` + + ```python + import os + api_key = os.environ["POKETWO_API_KEY"] + ``` + +3. **Rotate keys regularly.** Periodically revoke old API keys and generate new ones to minimize the impact of a potential leak. + +4. **Use HTTPS exclusively.** All requests to the Pokétwo API must be made over HTTPS. Never send credentials over unencrypted HTTP. + +5. **Validate the `state` parameter.** When using OAuth, always generate a unique, random `state` value for each authorization request and verify it in the callback to prevent cross-site request forgery (CSRF) attacks. + +6. **Restrict redirect URIs.** Register only the specific redirect URIs your application uses. Avoid wildcard or overly broad patterns. + +7. **Handle token expiry gracefully.** Implement automatic token refresh logic so your application can recover from expired tokens without user intervention. + +8. **Revoke tokens on logout.** When a user logs out of your application, revoke their access and refresh tokens to prevent unauthorized reuse. + +## Webhook Signature Verification + +If you register webhooks to receive event notifications, Pokétwo signs every webhook payload so you can verify it was not tampered with in transit. Each webhook request includes an `X-Signature-256` header containing an HMAC-SHA256 signature of the request body, computed using your webhook secret. + +### Verifying the Signature + +#### Python + +```python +import hashlib +import hmac +from flask import Flask, request, abort + +app = Flask(__name__) +WEBHOOK_SECRET = os.environ["POKETWO_WEBHOOK_SECRET"] + +@app.route("/webhook", methods=["POST"]) +def handle_webhook(): + signature = request.headers.get("X-Signature-256", "") + expected = "sha256=" + hmac.new( + WEBHOOK_SECRET.encode(), + request.data, + hashlib.sha256, + ).hexdigest() + + if not hmac.compare_digest(signature, expected): + abort(403, "Invalid signature") + + payload = request.json + # Process the event... + return "", 200 +``` + +#### JavaScript (Node.js) + +```javascript +const crypto = require("crypto"); +const express = require("express"); +const app = express(); + +const WEBHOOK_SECRET = process.env.POKETWO_WEBHOOK_SECRET; + +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + const signature = req.headers["x-signature-256"] || ""; + const expected = + "sha256=" + + crypto.createHmac("sha256", WEBHOOK_SECRET).update(req.body).digest("hex"); + + if ( + !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) + ) { + return res.status(403).send("Invalid signature"); + } + + const payload = JSON.parse(req.body); + // Process the event... + res.sendStatus(200); +}); +``` + +{% hint style="danger" %} +Always use constant-time comparison functions (`hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node.js) when verifying signatures. Standard string equality checks are vulnerable to timing attacks. +{% endhint %} + +{% hint style="info" %} +For questions about authentication or to report a security issue, reach out at [discord.gg/poketwo](https://discord.gg/poketwo). +{% endhint %} diff --git a/api/rate-limiting.md b/api/rate-limiting.md new file mode 100644 index 0000000..fd274cc --- /dev/null +++ b/api/rate-limiting.md @@ -0,0 +1,159 @@ +--- +description: >- + Learn about the Pokétwo API rate limits, how they work, and best practices for + handling them in your application. +--- + +# Rate Limiting + +{% hint style="warning" %} +This site is an early **work in progress**. Many pages may be missing or incomplete. Please let us know at [discord.gg/poketwo](https://discord.gg/poketwo) if you would like to help write or improve a page. +{% endhint %} + +The Pokétwo API enforces rate limits to ensure fair usage and protect the service from abuse. All API consumers must respect these limits to maintain access. + +## How Rate Limiting Works + +Rate limits restrict the number of requests a client can make within a given time window. When you exceed a rate limit, the API will respond with an **HTTP 429 Too Many Requests** status code. The response will include headers indicating when you can retry. + +### Rate Limit Headers + +Every API response includes the following headers: + +| Header | Description | +| ----------------------- | ------------------------------------------------------------------ | +| `X-RateLimit-Limit` | The maximum number of requests allowed in the current time window. | +| `X-RateLimit-Remaining` | The number of requests remaining in the current time window. | +| `X-RateLimit-Reset` | Unix timestamp (in seconds) when the current window resets. | +| `Retry-After` | Seconds to wait before retrying (only present on 429 responses). | + +## Endpoint Rate Limits + +Different endpoints have different rate limits depending on the nature of the operation. + +| Endpoint Category | Rate Limit | Time Window | Notes | +| ---------------------- | ------------------- | ----------- | -------------------------------- | +| **General (GET)** | 60 requests | 60 seconds | Standard read operations. | +| **Search / List** | 30 requests | 60 seconds | Pokémon search, market listings. | +| **Write (POST/PATCH)** | 20 requests | 60 seconds | Creating or updating resources. | +| **Authentication** | 10 requests | 60 seconds | Login, token refresh. | +| **Bulk Operations** | 5 requests | 60 seconds | Batch endpoints. | +| **Webhooks** | 30 requests | 60 seconds | Webhook delivery endpoints. | + +{% hint style="info" %} +Rate limits are applied **per API key**. If you have multiple API keys, each key has its own independent rate limit counters. +{% endhint %} + +## Handling Rate Limits + +### Reading Rate Limit Headers + +When making requests, always check the rate limit headers in the response to track your remaining quota. + +```python +import requests + +response = requests.get( + "https://api.poketwo.net/v1/pokemon", + headers={"Authorization": "Bearer YOUR_API_KEY"} +) + +remaining = int(response.headers.get("X-RateLimit-Remaining", 0)) +reset_time = int(response.headers.get("X-RateLimit-Reset", 0)) + +print(f"Requests remaining: {remaining}") +print(f"Window resets at: {reset_time}") +``` + +### Implementing Retry Logic + +When you receive a 429 response, use the `Retry-After` header to wait before retrying. + +```python +import time +import requests + +def make_request(url, headers): + while True: + response = requests.get(url, headers=headers) + + if response.status_code == 429: + retry_after = int(response.headers.get("Retry-After", 5)) + print(f"Rate limited. Retrying after {retry_after} seconds...") + time.sleep(retry_after) + continue + + return response +``` + +### JavaScript Example + +```javascript +async function makeRequest(url, apiKey) { + const response = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}` }, + }); + + if (response.status === 429) { + const retryAfter = parseInt(response.headers.get("Retry-After") || "5", 10); + console.log(`Rate limited. Retrying after ${retryAfter}s...`); + await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000)); + return makeRequest(url, apiKey); + } + + return response; +} +``` + +### Proactive Rate Limit Management + +Instead of waiting for a 429, you can proactively manage your request rate by reading the headers before each call. + +```python +import time +import requests + +class RateLimitedClient: + def __init__(self, api_key): + self.api_key = api_key + self.remaining = None + self.reset_time = None + + def request(self, method, url, **kwargs): + if self.remaining is not None and self.remaining <= 0: + wait_time = max(0, self.reset_time - time.time()) + if wait_time > 0: + print(f"Waiting {wait_time:.1f}s for rate limit reset...") + time.sleep(wait_time) + + response = requests.request( + method, url, + headers={"Authorization": f"Bearer {self.api_key}"}, + **kwargs + ) + + self.remaining = int(response.headers.get("X-RateLimit-Remaining", 0)) + self.reset_time = int(response.headers.get("X-RateLimit-Reset", 0)) + + return response +``` + +## Best Practices + +1. **Cache responses whenever possible.** Avoid making redundant requests for data that does not change frequently, such as Pokémon species information. + +2. **Use bulk endpoints.** When you need data for multiple resources, prefer batch or list endpoints over making many individual requests. + +3. **Implement exponential backoff.** If you are consistently rate limited, increase the delay between retries exponentially rather than retrying at a fixed interval. + +4. **Monitor your usage.** Track the `X-RateLimit-Remaining` header to understand your consumption patterns and adjust your request rate accordingly. + +5. **Avoid unnecessary polling.** Instead of polling for updates on a tight loop, use webhooks or longer polling intervals to reduce your request volume. + +6. **Spread requests over time.** If you have a batch of requests to make, distribute them evenly over the time window rather than sending them all at once. + +7. **Handle 429 errors gracefully.** Never ignore rate limit responses. Always wait the specified `Retry-After` duration before retrying. + +{% hint style="danger" %} +Repeatedly ignoring rate limits or attempting to circumvent them may result in your API key being revoked. Respect the limits to maintain uninterrupted access. +{% endhint %}