Skip to content

Developer Documentation Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build an MkDocs documentation site with tested code examples that stay in sync with the API via CI.

Architecture: Example code lives in examples/ as standalone runnable files with --8<-- snippet markers. Tests in tests/test_examples.py import/execute/validate those examples against the test API. MkDocs pages in docs/ pull snippets from examples/ via pymdownx.snippets. GitHub Actions deploys to GitHub Pages on push to main.

Tech Stack: MkDocs, mkdocs-material, pymdownx.snippets, pytest, FastAPI TestClient

Design doc: docs/plans/2026-05-13-developer-docs-design.md


Task 1: MkDocs Project Setup

Files: - Create: mkdocs.yml - Modify: requirements.txt:1-12

  • [ ] Step 1: Add MkDocs dependencies to requirements.txt

Append to the end of requirements.txt:

mkdocs
mkdocs-material
  • [ ] Step 2: Create mkdocs.yml
site_name: usnap.me Docs
site_url: https://granteagon.github.io/usnap.me
repo_url: https://github.com/granteagon/usnap.me
repo_name: granteagon/usnap.me

theme:
  name: material
  palette:
    - scheme: default
      toggle:
        icon: material/brightness-7
        name: Switch to dark mode
    - scheme: slate
      toggle:
        icon: material/brightness-4
        name: Switch to light mode

markdown_extensions:
  - pymdownx.snippets:
      base_path: "."
  - pymdownx.highlight:
      anchor_linenums: true
  - pymdownx.superfences
  - pymdownx.tabbed:
      alternate_style: true
  - admonition
  - pymdownx.details
  - toc:
      permalink: true

nav:
  - Home: index.md
  - Quickstart: quickstart.md
  - Code Examples: examples.md
  - API Reference: api-reference.md
  - Rate Limits & Quotas: rate-limits.md
  • [ ] Step 3: Create placeholder docs/index.md
# usnap.me

Placeholder — will be filled in Task 6.

This lets us verify the MkDocs build works before writing real content.

  • [ ] Step 4: Install dependencies and verify MkDocs builds

Run: pip install -r requirements.txt && mkdocs build --strict Expected: INFO - Documentation built in X.XX seconds with no errors.

  • [ ] Step 5: Commit
git add mkdocs.yml requirements.txt docs/index.md
git commit -m "feat: add MkDocs project setup with material theme"

Task 2: cURL Example File

Files: - Create: examples/create_link.sh

The shell script is a reference file — not meant to be executed directly. It contains cURL commands that tests/test_examples.py (Task 5) will parse and replay. Each snippet is delimited by marker comments.

  • [ ] Step 1: Create examples/create_link.sh
#!/usr/bin/env bash
# Example cURL commands for the usnap.me API.
# Each snippet is delimited by markers for MkDocs inclusion.

API_KEY="your-api-key"
BASE_URL="https://usnp.me"

curl -X POST "$BASE_URL/shorten" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_url": "https://example.com/landing"
  }'

curl -X POST "$BASE_URL/shorten" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_url": "https://example.com/product",
    "data": {"sku": "WIDGET-42", "campaign": "summer-2026"},
    "webhooks": ["https://your-server.com/webhook"]
  }'

curl -O -J "$BASE_URL/abc123/qr?format=png&scale=10"

curl "$BASE_URL/usage" \
  -H "X-API-Key: $API_KEY"
  • [ ] Step 2: Commit
git add examples/create_link.sh
git commit -m "feat: add cURL example file with snippet markers"

Task 3: Python Example File

Files: - Create: examples/create_link.py

  • [ ] Step 1: Create examples/create_link.py
"""Example: Create shortlinks using the usnap.me API with Python (requests)."""
import requests

API_KEY = "your-api-key"
BASE_URL = "https://usnp.me"

response = requests.post(
    f"{BASE_URL}/shorten",
    headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
    json={"redirect_url": "https://example.com/landing"},
)
print(response.status_code)
print(response.json())

response = requests.post(
    f"{BASE_URL}/shorten",
    headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
    json={
        "redirect_url": "https://example.com/product",
        "data": {"sku": "WIDGET-42", "campaign": "summer-2026"},
        "webhooks": ["https://your-server.com/webhook"],
    },
)
print(response.status_code)
print(response.json())
  • [ ] Step 2: Commit
git add examples/create_link.py
git commit -m "feat: add Python example file with snippet markers"

Task 4: JavaScript Example File

Files: - Create: examples/create_link.js

  • [ ] Step 1: Create examples/create_link.js
// Example: Create shortlinks using the usnap.me API with JavaScript (fetch).

const API_KEY = "your-api-key";
const BASE_URL = "https://usnp.me";

const basicResponse = await fetch(`${BASE_URL}/shorten`, {
  method: "POST",
  headers: {
    "X-API-Key": API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    redirect_url: "https://example.com/landing",
  }),
});
const basicResult = await basicResponse.json();
console.log(basicResult);

const dataResponse = await fetch(`${BASE_URL}/shorten`, {
  method: "POST",
  headers: {
    "X-API-Key": API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    redirect_url: "https://example.com/product",
    data: { sku: "WIDGET-42", campaign: "summer-2026" },
    webhooks: ["https://your-server.com/webhook"],
  }),
});
const dataResult = await dataResponse.json();
console.log(dataResult);
  • [ ] Step 2: Commit
git add examples/create_link.js
git commit -m "feat: add JavaScript example file with snippet markers"

Task 5: Webhook Receiver Examples

Files: - Create: examples/webhook_receiver.py - Create: examples/webhook_receiver.js

  • [ ] Step 1: Create examples/webhook_receiver.py
"""Example: Webhook receiver with HMAC signature verification (FastAPI)."""
import hashlib
import hmac
import json

def verify_webhook_signature(payload_bytes: bytes, signature_header: str, secret: str) -> bool:
    """Verify the X-Webhook-Signature header matches the payload."""
    expected = hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()
    received = signature_header.removeprefix("sha256=")
    return hmac.compare_digest(expected, received)

from fastapi import FastAPI, Request, HTTPException

receiver_app = FastAPI()
WEBHOOK_SECRET = "your-webhook-secret"

@receiver_app.post("/webhook")
async def handle_webhook(request: Request):
    body = await request.body()
    signature = request.headers.get("X-Webhook-Signature", "")

    if not verify_webhook_signature(body, signature, WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid signature")

    payload = json.loads(body)
    print(f"Link clicked: {payload['short_id']}")
    print(f"Custom data: {payload.get('data')}")
    return {"status": "ok"}
  • [ ] Step 2: Create examples/webhook_receiver.js
// Example: Webhook receiver with HMAC signature verification (Express.js).
const crypto = require("crypto");
const express = require("express");

function verifyWebhookSignature(payloadBytes, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payloadBytes)
    .digest("hex");
  const received = signatureHeader.replace("sha256=", "");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(received, "hex")
  );
}

const app = express();
const WEBHOOK_SECRET = "your-webhook-secret";

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-webhook-signature"] || "";

  if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const payload = JSON.parse(req.body);
  console.log(`Link clicked: ${payload.short_id}`);
  console.log(`Custom data: ${JSON.stringify(payload.data)}`);
  res.json({ status: "ok" });
});

app.listen(3000, () => console.log("Webhook receiver on :3000"));
  • [ ] Step 3: Commit
git add examples/webhook_receiver.py examples/webhook_receiver.js
git commit -m "feat: add webhook receiver examples with HMAC verification"

Task 6: Tests for All Example Files

Files: - Create: tests/test_examples.py

This test file validates that every code example stays in sync with the actual API. It tests: 1. Python examples: Import and execute the request-building code against the FastAPI test client. 2. cURL examples: Parse the shell commands to extract method, URL path, headers, and body. Replay as test client requests. Assert endpoints and payloads match the API. 3. JS examples: Parse the fetch calls to extract URL, method, headers, and body JSON. Assert they are syntactically correct and match the API contract (method, path, Content-Type header, body keys). 4. Webhook receivers: Call verify_webhook_signature directly with valid/invalid signatures.

  • [ ] Step 1: Write the cURL parsing test

This test extracts the cURL commands from examples/create_link.sh, parses them, and replays them against the test client.

"""Tests that verify all example code stays in sync with the API."""
import hashlib
import hmac
import json
import re
from pathlib import Path

import pytest


EXAMPLES_DIR = Path(__file__).parent.parent / "examples"


def extract_snippet(filepath: Path, snippet_name: str) -> str:
    """Extract a named snippet from a file with --8<-- markers."""
    text = filepath.read_text()
    pattern = rf"#\s*--8<--\s*\[start:{snippet_name}\]\n(.*?)#\s*--8<--\s*\[end:{snippet_name}\]"
    match = re.search(pattern, text, re.DOTALL)
    assert match, f"Snippet '{snippet_name}' not found in {filepath}"
    return match.group(1).strip()


def parse_curl_command(curl_str: str) -> dict:
    """
    Parse a cURL command string into method, path, headers, and body.
    Returns dict with keys: method, path, headers, body.
    """
    # Default method
    method = "GET"
    if "-X POST" in curl_str:
        method = "POST"

    # Extract URL path (replace $BASE_URL with empty string to get path)
    url_match = re.search(r'"?\$BASE_URL(/[^"'\s]*)"?', curl_str)
    path = url_match.group(1) if url_match else "/"

    # Extract headers
    headers = {}
    for h_match in re.finditer(r'-H\s+"([^"]+)"', curl_str):
        key, _, value = h_match.group(1).partition(": ")
        # Replace variable references with test values
        value = value.replace("$API_KEY", "test-api-key")
        headers[key] = value

    # Extract JSON body
    body = None
    body_match = re.search(r"-d\s+'({.*?})'", curl_str, re.DOTALL)
    if body_match:
        body = json.loads(body_match.group(1))

    return {"method": method, "path": path, "headers": headers, "body": body}


class TestCurlExamples:
    """Verify cURL examples match the actual API contract."""

    def test_create_basic_curl(self, test_client, mock_supabase):
        """The basic cURL example hits POST /shorten with correct shape."""
        snippet = extract_snippet(EXAMPLES_DIR / "create_link.sh", "create_basic")
        parsed = parse_curl_command(snippet)

        assert parsed["method"] == "POST"
        assert parsed["path"] == "/shorten"
        assert "X-API-Key" in parsed["headers"]
        assert "Content-Type" in parsed["headers"]
        assert "redirect_url" in parsed["body"]

        # Replay against test client
        mock_supabase.set_response("links", [])  # collision check
        mock_supabase.set_response("links", [{
            "id": "test-id",
            "short_id": "abc123",
            "redirect_url": parsed["body"]["redirect_url"],
            "created_at": "2026-01-01T00:00:00Z",
        }])

        response = test_client.post(
            parsed["path"],
            headers={"X-API-Key": "test-api-key", "Content-Type": "application/json"},
            json=parsed["body"],
        )
        assert response.status_code == 200

    def test_create_with_data_curl(self, test_client, mock_supabase):
        """The data+webhooks cURL example hits POST /shorten with correct shape."""
        snippet = extract_snippet(EXAMPLES_DIR / "create_link.sh", "create_with_data")
        parsed = parse_curl_command(snippet)

        assert parsed["method"] == "POST"
        assert parsed["path"] == "/shorten"
        assert "redirect_url" in parsed["body"]
        assert "data" in parsed["body"]
        assert "webhooks" in parsed["body"]

        mock_supabase.set_response("links", [])
        mock_supabase.set_response("links", [{
            "id": "test-id",
            "short_id": "abc123",
            "redirect_url": parsed["body"]["redirect_url"],
            "created_at": "2026-01-01T00:00:00Z",
        }])

        response = test_client.post(
            parsed["path"],
            headers={"X-API-Key": "test-api-key", "Content-Type": "application/json"},
            json=parsed["body"],
        )
        assert response.status_code == 200

    def test_generate_qr_curl(self):
        """The QR code cURL example targets the correct endpoint."""
        snippet = extract_snippet(EXAMPLES_DIR / "create_link.sh", "generate_qr")
        # Should reference /{short_id}/qr endpoint
        assert "/qr" in snippet
        assert "format=png" in snippet or "format=svg" in snippet or "/qr" in snippet

    def test_check_usage_curl(self):
        """The usage cURL example targets GET /usage with API key header."""
        snippet = extract_snippet(EXAMPLES_DIR / "create_link.sh", "check_usage")
        parsed = parse_curl_command(snippet)

        assert parsed["method"] == "GET"
        assert parsed["path"] == "/usage"
        assert "X-API-Key" in parsed["headers"]
  • [ ] Step 2: Run cURL tests to verify they fail (examples don't exist yet if running standalone)

Run: pytest tests/test_examples.py::TestCurlExamples -v Expected: All 4 tests pass (examples were created in Tasks 2-4).

  • [ ] Step 3: Write the Python example test

Append to tests/test_examples.py:

class TestPythonExamples:
    """Verify Python examples match the actual API contract."""

    def test_create_basic_python(self):
        """The basic Python example uses correct endpoint, method, headers, and body keys."""
        snippet = extract_snippet(EXAMPLES_DIR / "create_link.py", "create_basic")

        # Verify it calls POST /shorten
        assert "requests.post" in snippet
        assert "/shorten" in snippet
        assert "X-API-Key" in snippet
        assert "redirect_url" in snippet

    def test_create_with_data_python(self):
        """The data+webhooks Python example includes data and webhooks fields."""
        snippet = extract_snippet(EXAMPLES_DIR / "create_link.py", "create_with_data")

        assert "requests.post" in snippet
        assert "/shorten" in snippet
        assert '"redirect_url"' in snippet
        assert '"data"' in snippet
        assert '"webhooks"' in snippet
  • [ ] Step 4: Run Python example tests

Run: pytest tests/test_examples.py::TestPythonExamples -v Expected: PASS

  • [ ] Step 5: Write the JavaScript example test

Append to tests/test_examples.py:

class TestJavaScriptExamples:
    """Verify JavaScript examples match the actual API contract."""

    def test_create_basic_js(self):
        """The basic JS example uses correct endpoint, method, headers, and body keys."""
        snippet = extract_snippet(EXAMPLES_DIR / "create_link.js", "create_basic")

        assert "fetch(" in snippet
        assert "/shorten" in snippet
        assert '"POST"' in snippet
        assert "X-API-Key" in snippet
        assert "redirect_url" in snippet

    def test_create_with_data_js(self):
        """The data+webhooks JS example includes data and webhooks fields."""
        snippet = extract_snippet(EXAMPLES_DIR / "create_link.js", "create_with_data")

        assert "fetch(" in snippet
        assert "/shorten" in snippet
        assert "redirect_url" in snippet
        assert "data:" in snippet or '"data"' in snippet
        assert "webhooks:" in snippet or '"webhooks"' in snippet
  • [ ] Step 6: Run JS example tests

Run: pytest tests/test_examples.py::TestJavaScriptExamples -v Expected: PASS

  • [ ] Step 7: Write the webhook receiver tests

Append to tests/test_examples.py:

class TestWebhookReceiverExamples:
    """Verify webhook receiver examples correctly implement HMAC verification."""

    def test_python_verify_valid_signature(self):
        """Python verify_webhook_signature accepts a valid HMAC signature."""
        import importlib.util
        spec = importlib.util.spec_from_file_location(
            "webhook_receiver", EXAMPLES_DIR / "webhook_receiver.py"
        )
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)

        secret = "test-secret-123"
        payload = json.dumps({"short_id": "abc123", "data": {"sku": "W1"}}).encode()
        signature = "sha256=" + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()

        assert mod.verify_webhook_signature(payload, signature, secret) is True

    def test_python_verify_invalid_signature(self):
        """Python verify_webhook_signature rejects an invalid HMAC signature."""
        import importlib.util
        spec = importlib.util.spec_from_file_location(
            "webhook_receiver", EXAMPLES_DIR / "webhook_receiver.py"
        )
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)

        secret = "test-secret-123"
        payload = json.dumps({"short_id": "abc123"}).encode()

        assert mod.verify_webhook_signature(payload, "sha256=badhex000", secret) is False

    def test_js_verify_signature_structure(self):
        """JS webhook receiver uses crypto.createHmac with sha256 and timingSafeEqual."""
        snippet = extract_snippet(EXAMPLES_DIR / "webhook_receiver.js", "verify_signature")

        assert "createHmac" in snippet
        assert '"sha256"' in snippet
        assert "timingSafeEqual" in snippet

    def test_js_receiver_checks_signature(self):
        """JS Express receiver calls verifyWebhookSignature before processing."""
        snippet = extract_snippet(EXAMPLES_DIR / "webhook_receiver.js", "express_receiver")

        # Verify it checks signature before processing the payload
        sig_check_pos = snippet.find("verifyWebhookSignature")
        parse_pos = snippet.find("JSON.parse")
        assert sig_check_pos != -1, "Must call verifyWebhookSignature"
        assert parse_pos != -1, "Must parse JSON payload"
        assert sig_check_pos < parse_pos, "Must verify signature before parsing payload"
  • [ ] Step 8: Run all example tests

Run: pytest tests/test_examples.py -v Expected: All tests pass.

  • [ ] Step 9: Run full test suite

Run: pytest --cov=. --cov-report=term-missing Expected: All tests pass (existing + new).

  • [ ] Step 10: Commit
git add tests/test_examples.py
git commit -m "feat: add tests for all example code files"

Task 7: Documentation Pages

Files: - Create: docs/index.md - Create: docs/quickstart.md - Create: docs/examples.md - Create: docs/api-reference.md - Create: docs/rate-limits.md

All code blocks in these pages MUST use --8<-- snippet inclusion from examples/ — no hand-written code blocks for API usage.

  • [ ] Step 1: Write docs/index.md
# usnap.me

**Shortlinks that carry your data.**

usnap.me is a shortlink API that attaches custom JSON data to every link. When someone clicks
a link, your webhooks fire instantly with the data you embedded — SKU codes, campaign tags,
customer IDs, anything you need.

## Use Cases

- **Packaging & print QR codes** — Embed a SKU or batch number. When scanned, your system
  gets a webhook with the product data.
- **Campaign attribution** — Tag links with campaign and source metadata. Every click delivers
  attribution data to your analytics pipeline.
- **Real-time event triggers** — Fire webhooks on click to start workflows, send notifications,
  or update dashboards.

## Get Started

Follow the [Quickstart](quickstart.md) to create your first shortlink in under a minute.

Explore the [API Reference](api-reference.md) for the full endpoint documentation, or
try the interactive [Swagger UI](https://usnp.me/docs).
  • [ ] Step 2: Write docs/quickstart.md
# Quickstart

Create a data-carrying shortlink, click it, and see your webhook fire — all in five steps.

## 1. Get an API Key

You need an API key to create links. If you have the master key, skip to step 2. Otherwise,
create a scoped key:

```bash
curl -X POST https://usnp.me/api-keys \
  -H "X-API-Key: YOUR_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label": "my first key"}'

Save the key and webhook_secret from the response.

Create a basic shortlink that redirects to your target URL:

--8<-- "examples/create_link.sh:create_basic"

The response includes your short_url (e.g., https://usnp.me/abc123).

Attach custom data and register a webhook to receive it on every click:

--8<-- "examples/create_link.sh:create_with_data"

Open the short_url in your browser. usnap.me will:

  1. Redirect you to the target URL
  2. Record the hit
  3. POST your custom data to each registered webhook

Your webhook receives a JSON payload like:

{
  "event": "hit",
  "short_id": "abc123",
  "redirect_url": "https://example.com/product",
  "hit_id": "...",
  "hit_at": "2026-01-15T10:30:00Z",
  "data": {"sku": "WIDGET-42", "campaign": "summer-2026"}
}

5. Generate a QR Code

Generate a printable QR code for any shortlink:

--8<-- "examples/create_link.sh:generate_qr"

Customize colors, format (PNG/SVG), and scale — see the API Reference.

**Important:** The `;` before `--8<--` is required by `pymdownx.snippets` when including inside fenced code blocks. The `;` is stripped during rendering.

- [ ] **Step 3: Write docs/examples.md**

```markdown
# Code Examples

Complete working examples in Python and JavaScript. All code on this page is tested in CI
to stay in sync with the API.

## Create a Shortlink

=== "Python"

    ```python
    --8<-- "examples/create_link.py:create_basic"
    ```

=== "JavaScript"

    ```javascript
    --8<-- "examples/create_link.js:create_basic"
    ```

=== "cURL"

    ```bash
    --8<-- "examples/create_link.sh:create_basic"
    ```

## Create a Link with Data and Webhooks

=== "Python"

    ```python
    --8<-- "examples/create_link.py:create_with_data"
    ```

=== "JavaScript"

    ```javascript
    --8<-- "examples/create_link.js:create_with_data"
    ```

=== "cURL"

    ```bash
    --8<-- "examples/create_link.sh:create_with_data"
    ```

## Webhook Receiver

Verify the `X-Webhook-Signature` header to ensure payloads come from usnap.me.

=== "Python (FastAPI)"

    ```python
    --8<-- "examples/webhook_receiver.py:verify_signature"
    ```

    Full receiver:

    ```python
    --8<-- "examples/webhook_receiver.py:fastapi_receiver"
    ```

=== "JavaScript (Express.js)"

    ```javascript
    --8<-- "examples/webhook_receiver.js:verify_signature"
    ```

    Full receiver:

    ```javascript
    --8<-- "examples/webhook_receiver.js:express_receiver"
    ```

  • [ ] Step 4: Write docs/api-reference.md
# API Reference

Base URL: `https://usnp.me`

For interactive exploration, use the [Swagger UI](https://usnp.me/docs).

## Authentication

All endpoints except `GET /{short_id}` (redirect) and `GET /{short_id}/qr` (QR code)
require an `X-API-Key` header.

## Endpoints

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/` | No | Health check |
| `POST` | `/shorten` | Yes | Create a shortlink |
| `GET` | `/{short_id}` | No | Redirect to target URL |
| `GET` | `/{short_id}/qr` | No | Generate QR code |
| `GET` | `/{short_id}/webhooks/status` | Yes | Webhook delivery status |
| `POST` | `/api-keys` | Yes | Create a new API key |
| `GET` | `/api-keys` | Yes | List API keys |
| `DELETE` | `/api-keys/{key_id}` | Yes | Revoke an API key |
| `POST` | `/api-keys/{key_id}/rotate` | Yes | Rotate an API key |
| `GET` | `/usage` | Yes | Usage summary for current month |

## POST /shorten

Create a new shortlink with an optional data payload and webhook URLs.

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `redirect_url` | string (URL) | Yes | Target URL for the redirect |
| `data` | object | No | Custom JSON data (max 10 KB) delivered to webhooks |
| `webhooks` | array of URLs | No | Webhook endpoints to notify on each click |

**Response (200):**

```json
{
  "short_id": "abc123",
  "short_url": "https://usnp.me/abc123",
  "redirect_url": "https://example.com/landing",
  "created_at": "2026-01-15T10:30:00Z"
}

Response headers (for scoped API keys):

Header Description
X-Usage-Links-Created Links created this month
X-Usage-Links-Limit Monthly link creation limit (or unlimited)

GET /{short_id}

Resolves a shortlink and returns a 302 redirect. Records the hit and fires webhooks in the background.

GET /{short_id}/qr

Generate a QR code image for a shortlink.

Query parameters:

Parameter Type Default Description
format png or svg png Output image format
scale int (1-20) 5 Size multiplier
dark string #000000 QR module color (hex or CMYK)
light string #FFFFFF Background color (hex, CMYK, or transparent)

Webhook Payload

When a shortlink is clicked, each registered webhook receives a POST with:

{
  "event": "hit",
  "short_id": "abc123",
  "redirect_url": "https://example.com/product",
  "hit_id": "550e8400-...",
  "hit_at": "2026-01-15T10:30:00Z",
  "data": {"sku": "WIDGET-42"}
}

Headers:

Header Description
X-Webhook-Signature sha256=<HMAC-SHA256 hex digest> — sign the raw JSON body with your webhook_secret to verify
Content-Type application/json

Webhooks retry up to 3 times with exponential backoff (1s, 5s delays).

GET /usage

Returns usage counters for the current month.

Response:

{
  "api_key_id": "550e8400-...",
  "month": "2026-05",
  "usage": {
    "links_created": {"current": 42, "limit": 100},
    "hits_served": {"current": 1583, "limit": null},
    "webhook_deliveries": {"current": 312, "limit": 1000}
  }
}

limit: null means unlimited for that metric.

- [ ] **Step 5: Write docs/rate-limits.md**

```markdown
# Rate Limits & Quotas

## Rate Limits

Per-minute rate limits based on your API key tier:

| Endpoint | Free | Pro |
|----------|------|-----|
| `POST /shorten` | 10/min | 60/min |
| `GET /{short_id}` | 100/min | 100/min |

Rate-limited responses return `429 Too Many Requests` with a `Retry-After` header.

## Monthly Quotas

| Metric | Free | Pro |
|--------|------|-----|
| Links created | 100 | Unlimited |
| Hits served | Unlimited (tracked) | Unlimited (tracked) |
| Webhook deliveries | 1,000 | 50,000 |

When a quota is exceeded:

- **Link creation:** Returns `403` with `{"detail": "Monthly link creation limit reached", "limit": 100, "current": 100}`.
- **Webhook deliveries:** Delivery is skipped and logged as failed with error `"quota exceeded"`.
- **Hits:** Never limited — existing links always work.

## Check Your Usage

```bash
--8<-- "examples/create_link.sh:check_usage"

See the GET /usage endpoint for response details.

- [ ] **Step 6: Verify MkDocs builds with all pages**

Run: `mkdocs build --strict`
Expected: Build succeeds with no warnings about missing snippets or broken links.

- [ ] **Step 7: Commit**

```bash
git add docs/
git commit -m "feat: add documentation pages with tested snippet inclusion"


Task 8: Replace README.md

Files: - Modify: README.md

  • [ ] Step 1: Replace README.md

# usnap.me

**Shortlinks that carry your data.**

[![Docs](https://img.shields.io/badge/docs-granteagon.github.io%2Fusnap.me-blue)](https://granteagon.github.io/usnap.me)

usnap.me is a shortlink API that attaches custom JSON data to every link.
When someone clicks a link, your webhooks fire instantly with the embedded data.

**[Get Started](https://granteagon.github.io/usnap.me/quickstart/)** |
[API Reference](https://granteagon.github.io/usnap.me/api-reference/) |
[Swagger UI](https://usnp.me/docs)

## Development

```bash
# Install dependencies
pip install -r requirements.txt

# Run dev server
uvicorn main:app --reload

# Run tests
pytest --cov=. --cov-report=term-missing

# Build docs locally
mkdocs serve
- [ ] **Step 2: Commit**

```bash
git add README.md
git commit -m "feat: replace README with landing page linking to docs site"


Task 9: GitHub Actions — Deploy Docs

Files: - Modify: .github/workflows/test.yml:1-104

Add a docs job that runs mkdocs gh-deploy on push to main, after tests pass.

  • [ ] Step 1: Add docs deploy job to .github/workflows/test.yml

Add this job after the existing deploy job:

  docs:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Set up Python 3.11
      uses: actions/setup-python@v5
      with:
        python-version: '3.11'

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install mkdocs mkdocs-material

    - name: Deploy docs to GitHub Pages
      run: mkdocs gh-deploy --force
  • [ ] Step 2: Verify the workflow YAML is valid

Run: python -c "import yaml; yaml.safe_load(open('.github/workflows/test.yml'))" Expected: No error (exits cleanly).

Note: If pyyaml is not installed, run pip install pyyaml first or just visually verify indentation. The CI will validate it on push.

  • [ ] Step 3: Commit
git add .github/workflows/test.yml
git commit -m "ci: add docs deployment to GitHub Pages on push to main"

Task 10: Final Verification

Files: None (verification only)

  • [ ] Step 1: Run full test suite

Run: pytest --cov=. --cov-report=term-missing -v Expected: All tests pass, including the new tests/test_examples.py tests.

  • [ ] Step 2: Build docs and verify

Run: mkdocs build --strict Expected: Builds with no errors or warnings.

  • [ ] Step 3: Serve docs locally and spot-check

Run: mkdocs serve (then open http://127.0.0.1:8000 in a browser) Verify: - Home page shows tagline and use cases - Quickstart shows cURL snippets rendered from examples/ - Code Examples page has tabs for Python/JS/cURL - API Reference has endpoint table - Rate Limits page has tier tables

  • [ ] Step 4: Verify snippet rendering

Check that no pages show raw --8<-- markers — all snippets should be replaced with actual code content.

  • [ ] Step 5: Report results

Report test count, coverage percentage, and MkDocs build status. List any issues found.