Claude API Integration with Django Backend
Integrating the Claude API into a Django backend comes down to three decisions: where you put the API call (a service module, not your views), how you handle latency (sync request/response vs. streaming vs. background tasks), and how you manage the API key across environments. This article walks through a working setup for all three, plus the gotchas that show up once you move past a proof of concept.
If you just want the fastest path, the short version is: wrap Claude calls in a dedicated service class, use httpx or the official anthropic SDK, stream responses through StreamingHttpResponse for chat-style UIs, and push anything longer than a few seconds into Celery. The rest of this guide covers each piece in detail.
Project setup
Install the SDK and add your key to environment variables — never hardcode it in settings.py.
pip install anthropic django-environ
# settings.py
import environ
env = environ.Env()
environ.Env.read_env()
CLAUDE_API_KEY = env("CLAUDE_API_KEY")
CLAUDE_MODEL = env("CLAUDE_MODEL", default="claude-sonnet-4-20250514")
Keep the key out of version control and out of your Django admin. If multiple developers or environments need access, that's already a sign you want per-key usage tracking rather than one shared secret passed around in Slack — more on that below.
Building a service layer
Don't call the Claude SDK directly from views. A thin service class keeps your views testable and makes it trivial to swap providers or add retries later.
# services/claude.py
import anthropic
from django.conf import settings
class ClaudeService:
def __init__(self):
self.client = anthropic.Anthropic(api_key=settings.CLAUDE_API_KEY)
def send_message(self, messages, system=None, max_tokens=1024):
response = self.client.messages.create(
model=settings.CLAUDE_MODEL,
max_tokens=max_tokens,
system=system,
messages=messages,
)
return response.content[0].text
# views.py
from django.http import JsonResponse
from django.views import View
from .services.claude import ClaudeService
class ChatView(View):
def post(self, request):
import json
body = json.loads(request.body)
service = ClaudeService()
reply = service.send_message(
messages=[{"role": "user", "content": body["message"]}],
system="You are a helpful support assistant.",
)
return JsonResponse({"reply": reply})
This is enough for internal tools or low-traffic endpoints. For anything user-facing, you need to think about streaming and timeouts.
Streaming responses through Django
Claude's streaming API sends output incrementally, which matters for chat UIs where users expect to see text appear as it's generated. Django supports this via StreamingHttpResponse.
from django.http import StreamingHttpResponse
def stream_claude_response(request):
client = anthropic.Anthropic(api_key=settings.CLAUDE_API_KEY)
message = request.GET.get("message", "")
def event_stream():
with client.messages.stream(
model=settings.CLAUDE_MODEL,
max_tokens=1024,
messages=[{"role": "user", "content": message}],
) as stream:
for text in stream.text_stream:
yield f"data: {text}\n\n"
return StreamingHttpResponse(event_stream(), content_type="text/event-stream")
A few things trip people up here: gunicorn workers with sync WSGI will hold a worker for the full duration of the stream, so under load you'll want either an ASGI server (Daphne, Uvicorn) with async views, or a worker pool sized for concurrent streams rather than instant requests.
Async views for better concurrency
Django's async view support pairs well with Claude's async client, especially if your Django app is already running on ASGI.
from django.http import JsonResponse
import anthropic
async def async_chat_view(request):
client = anthropic.AsyncAnthropic(api_key=settings.CLAUDE_API_KEY)
body = json.loads(request.body)
response = await client.messages.create(
model=settings.CLAUDE_MODEL,
max_tokens=1024,
messages=[{"role": "user", "content": body["message"]}],
)
return JsonResponse({"reply": response.content[0].text})
Mixing sync ORM calls inside an async view requires sync_to_async wrappers — if your view also needs to hit the database, factor that out clearly rather than nesting it inline.
Offloading long requests to Celery
For workflows like document analysis or multi-step tool use where a single request might take 30+ seconds, don't block the HTTP request at all. Fire a Celery task, return a task ID, and let the frontend poll or use websockets for the result.
from celery import shared_task
@shared_task
def generate_claude_response(prompt, task_id):
service = ClaudeService()
result = service.send_message(messages=[{"role": "user", "content": prompt}])
# store result keyed by task_id, e.g. in Redis or a model
return result
This keeps your Django workers free and gives you a natural place to add retry logic if Claude's API returns a rate limit or overload error.
Error handling and retries
Claude API calls can fail for the usual reasons — network timeouts, rate limits, malformed requests. Wrap calls with explicit handling rather than letting exceptions bubble into a 500:
from anthropic import APIStatusError, APIConnectionError
def send_message(self, messages, **kwargs):
try:
return self.client.messages.create(messages=messages, **kwargs)
except APIStatusError as e:
if e.status_code == 429:
# back off and retry
pass
raise
except APIConnectionError:
# log and surface a friendly error
raise
Simplifying key management with SubToAPI
One recurring pain point in Django integrations is API key sprawl: separate keys for dev, staging, production, and per-developer usage that's impossible to attribute after the fact when the bill arrives. SubToAPI sits in front of Claude and gives you application-scoped keys (sub_live_...) with per-key usage metadata, so you can issue a distinct key per Django environment or per team member without changing anything else about your integration — it's a drop-in Bearer token change.
import requests
response = requests.post(
"https://api.subtoapi.app/v1/messages",
headers={"Authorization": f"Bearer {settings.SUBTOAPI_KEY}"},
json={
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Summarize this ticket"}],
},
)
It supports the same streaming and tool use patterns described above, so the Django-side code barely changes — see the quickstart, messages docs, streaming docs, and tool use docs. Plans start at €9/month with a free trial, listed on the pricing page.
Production checklist
- Store the API key in environment variables, never in code or Django admin fields
- Set explicit timeouts on all Claude requests (
timeout=30on the SDK client) - Log request/response metadata (not full content) for debugging, respecting user privacy
- Use Celery or async views for anything that might exceed typical HTTP timeout windows
- Rate-limit your own endpoints that call Claude, separate from Claude's own rate limits
- Version your prompts and system messages the same way you version code
questions
Should I use the anthropic Python SDK or raw HTTP requests in Django? Use the SDK for standard message calls — it handles retries, streaming, and typing for you. Raw requests or httpx calls make sense if you're building a lightweight wrapper or proxying through a service like SubToAPI with a plain REST interface.
How do I handle Claude API rate limits in a multi-tenant Django app? Implement application-level rate limiting per tenant before requests hit Claude, use exponential backoff on 429 responses, and consider per-tenant API keys so usage and limits are isolated rather than shared across your whole user base.
Can I run Claude API calls inside Django signals or model save methods? Technically yes, but avoid it — synchronous external API calls inside signals block the request/response cycle and make failures hard to trace. Move that logic into a service call triggered explicitly from a view or a Celery task instead.