FastAPI service for AI logo generation with credit management, rate limiting, and webhook delivery.
# Start all services (API + Worker + Redis)
docker compose up --build
# API available at http://localhost:8000On startup, the API seeds two test clients and outputs their credentials:
[seed] Rich Client: ld_live_...
[seed] Poor Client: ld_live_...
- Rich Client: 1000 credits (plenty for testing)
- Poor Client: 7 credits (enough for three 512px or one 1024px generation, but not enough for 2048px — good for testing insufficient credits)
Note: Seed balances are defined in app/services/client_service.py. To change them, edit the file and restart with docker compose down && docker compose up to flush Redis and re-seed.
- Python 3.11+
- Redis 7+
# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Create .env file
cp .env.example .env
# Edit .env for local Redis
# Change REDIS_URL=redis://redis:6379/0 to:
# REDIS_URL=redis://localhost:6379/0
# Or use sed to replace automatically:
sed -i 's/redis:\/\/redis:/redis:\/\/localhost:/g' .env
# Start Redis (if not running)
redis-server
# Run API
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Run worker (in separate terminal)
source venv/bin/activate
celery -A worker.celery_app worker --loglevel=infopytest tests/ -vTests cover credit system atomicity, rate limiter accuracy, and generation service operations.
Interactive API documentation available at http://localhost:8000/docs
All endpoints require an API key in the Authorization header:
Authorization: Bearer ld_live_...
Submit a new generation request.
Request:
{
"prompt": "modern coffee shop logo",
"style": "minimalist",
"output_size": "1024"
}Parameters:
prompt(string, required) — text descriptionstyle(string, optional) — style preferenceoutput_size(enum, optional) — "512", "1024", or "2048" (default: "1024")
Credit costs:
- 512px: 2 credits
- 1024px: 5 credits
- 2048px: 10 credits
Response (202 Accepted):
{
"id": "gen_AbCd1234",
"status": "queued",
"created_at": "2026-05-27T15:30:00Z"
}Errors:
401— invalid or missing API key402— insufficient credits429— rate limit exceeded (max 5 concurrent per client)503— generation queue unavailable
Poll generation status.
Response:
{
"id": "gen_AbCd1234",
"status": "completed",
"created_at": "2026-05-27T15:30:00Z",
"updated_at": "2026-05-27T15:30:12Z",
"result_url": "https://cdn.logodiffusion.com/fake/gen_AbCd1234.png"
}Status values:
queued— waiting for workerprocessing— AI model runningcompleted— result ready (includesresult_url)failed— generation failed (includeserrorobject)
Failed example:
{
"id": "gen_AbCd1234",
"status": "failed",
"created_at": "2026-05-27T15:30:00Z",
"updated_at": "2026-05-27T15:30:08Z",
"error": {
"code": "generation_failed",
"message": "AI provider returned an error"
}
}Errors:
404— generation not found (or belongs to different client)
Get current client information.
Response:
{
"id": "client_rich",
"name": "Rich Client",
"balance": 1000,
"webhook_url": "https://example.com/webhooks/generations"
}Register webhook URL for generation notifications.
Request:
{
"webhook_url": "https://example.com/webhooks/generations"
}Response (200 OK):
{
"webhook_url": "https://example.com/webhooks/generations"
}Webhook payload (POST):
{
"generation_id": "gen_AbCd1234",
"status": "completed",
"timestamp": "2026-05-27T15:30:12Z",
"result_url": "https://cdn.logodiffusion.com/fake/gen_AbCd1234.png"
}Service health and metrics.
Response:
{
"status": "ok",
"redis": "ok",
"celery": "ok",
"metrics": {
"total_generations": 42,
"completed": 35,
"failed": 7,
"queue_depth": 3
}
}# Get API key from startup logs
API_KEY="ld_live_..."
# Submit generation
RESPONSE=$(curl -s -X POST http://localhost:8000/v1/generations \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "tech startup logo", "output_size": "1024"}')
GEN_ID=$(echo $RESPONSE | jq -r '.id')
echo "Generation ID: $GEN_ID"
# Poll status (wait 5-15 seconds for mock AI)
curl http://localhost:8000/v1/generations/$GEN_ID \
-H "Authorization: Bearer $API_KEY"
# Get account info (balance, webhook, etc)
curl http://localhost:8000/v1/account \
-H "Authorization: Bearer $API_KEY"
# Register webhook
curl -X PATCH http://localhost:8000/v1/account/webhook \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://webhook.site/your-unique-id"}'
# Check service health
curl http://localhost:8000/v1/health- FastAPI — async HTTP API with lifespan event handlers
- Celery — background task queue for AI generation (mocked with random delay + success/failure)
- Redis — broker, backend, and datastore (client state, generation state, metrics)
- Lua scripts — atomic credit reserve/release and rate limiting
- Webhooks — fire-and-forget delivery in background threads
Credits are reserved on submission and released on failure:
- Client submits generation → credits deducted immediately
- Generation fails → credits returned to balance
- Generation succeeds → credits remain deducted
This prevents concurrent requests from overdrawing the balance.
Per-client concurrency limit: 5 simultaneous generations
Enforced with Redis atomic counter and TTL. Slots released when generation completes or fails.
All errors return structured JSON:
{
"error": {
"code": "insufficient_credits",
"message": "Not enough credits. This request requires 5 credits."
}
}Error codes:
invalid_request— validation failedunauthorized— invalid/missing API keyinsufficient_credits— not enough creditsrate_limit_exceeded— too many concurrent generationsnot_found— generation not foundgeneration_failed— AI provider errorgeneration_timeout— task exceeded time limitservice_unavailable— queue connection failedinternal_error— unexpected server error
app/
├── api/v1/ # API endpoints (generations, account, health)
├── core/ # Configuration, exceptions, shared utilities
├── models/ # Domain models (Client, Generation)
├── schemas/ # Request/response schemas, validation
├── services/ # Business logic (credits, rate limiting, webhooks)
└── storage/ # Redis connection pool management
worker/
├── tasks/ # Celery tasks (generation processing)
├── celery_app.py # Celery configuration
└── redis_client.py # Sync Redis client for workers
shared/
├── lua/ # Atomic Redis operations (credit reserve/release, rate limit)
└── redis_keys.py # Centralized key naming to prevent collisions
tests/ # Unit tests for credit system, rate limiter, generation service
The API and worker are separated but share common code through shared/. Services contain business logic isolated from HTTP and task layers, making them independently testable. Lua scripts live in shared/ since both API (reserve) and worker (release) need them.
Redis Lua scripts execute atomically, preventing race conditions when multiple requests check and deduct credits simultaneously. Alternative approaches (WATCH/MULTI/EXEC or distributed locks) are more complex and error-prone.
The reserve_credits.lua script atomically checks balance and decrements in a single Redis operation, ensuring the "three concurrent 5-credit requests with 12 credits" scenario correctly rejects the third request.
FastAPI uses async Redis (redis.asyncio) while Celery workers use sync Redis (redis.Redis). This prevents event loop conflicts and follows each framework's concurrency model.
Credits are deducted immediately on submission (not just "reserved"), then refunded on failure. This is simpler than a two-phase freeze/commit system and prevents the "reserved but never resolved" state if a worker crashes before committing.
When client A requests client B's generation, we return 404 (not 401/403) to avoid leaking information about which generation IDs exist. This prevents enumeration attacks.
Webhooks dispatch in background daemon threads with a 5-second timeout. We don't retry or log failures — the client can poll /v1/generations/{id} if the webhook doesn't arrive. This keeps the system simple and prevents webhook delivery issues from blocking generation processing.
We use Redis as broker, result backend, client state store, and metrics store. For a production system serving multiple clients, you'd separate these (dedicated message broker, persistent database for client state). For this scale, Redis handles it cleanly.
Added GET /v1/account to expose balance and settings, and nested webhook config under /v1/account/webhook. This groups account-related operations under a clear namespace and makes the API more discoverable for API consumers.
- Persistent storage: Move client accounts and generation history to PostgreSQL. Redis is great for ephemeral state, but client balances and audit logs need durability. Replace hardcoded seed clients with proper account management.
- Proper secrets management: API keys should be rotated, scoped by permission, and stored hashed with bcrypt (not SHA-256).
- Observability: Structured logging (structlog), distributed tracing (OpenTelemetry), and Prometheus metrics export.
- Webhook retries: Exponential backoff with a dead-letter queue for failed deliveries.
- Batch generations: Submit multiple prompts in one request, track as a batch job.
- Generation history:
/v1/generations?limit=50&cursor=...to list and filter client's past generations. - Idempotency keys: Let clients safely retry requests without creating duplicate generations.
- Priority queue: Premium clients get faster processing (separate Celery queue).
- Usage analytics: Detailed reports on credit consumption, success/failure rates, popular styles.
- Admin API: Separate endpoints for internal tools to create clients, adjust balances, view metrics (currently clients are hardcoded in seed).
- Integration tests: Full request → worker → webhook flow with a real Redis instance.
- Concurrency tests: Spawn 10 concurrent requests and verify credit/rate-limit correctness.
- Chaos testing: Kill worker mid-task and verify credits are released correctly.
- OpenAPI customization: Add request/response examples to auto-generated docs.
- SDK: Python/TypeScript client libraries wrapping the API.
- Webhook signature verification: HMAC-signed payloads so clients can verify authenticity.