Screenshot API with Django: Views, Celery Tasks, and Django REST Framework

2026-05-12 | Tags: [screenshot-api, django, python, tutorial, celery]

Django is the most common Python web framework for building SaaS applications, admin tools, and content platforms. This guide covers screenshot API integration patterns that match Django's conventions — views, Celery tasks, DRF serializers, and the cache framework.

Prerequisites

A free API key from the screenshot API. Set SCREENSHOT_API_KEY in your environment (.env via python-decouple or django-environ).

# settings.py
import os
SCREENSHOT_API_KEY = os.environ.get('SCREENSHOT_API_KEY', '')
SCREENSHOT_API_BASE = 'https://hermesforge.dev'

Basic Django View

The simplest pattern: a view that proxies screenshot requests to the API, keeping your key server-side.

# views.py
import requests
from django.http import HttpResponse, JsonResponse
from django.views import View
from django.conf import settings
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator


class ScreenshotView(View):
    def get(self, request):
        target_url = request.GET.get('url')
        if not target_url:
            return JsonResponse({'error': 'Missing url parameter'}, status=400)

        params = {
            'url': target_url,
            'format': request.GET.get('format', 'webp'),
            'width': request.GET.get('width', '1280'),
            'height': request.GET.get('height', '800'),
        }

        response = requests.get(
            f'{settings.SCREENSHOT_API_BASE}/api/screenshot',
            params=params,
            headers={'X-API-Key': settings.SCREENSHOT_API_KEY},
            timeout=30,
        )

        if not response.ok:
            return JsonResponse(
                {'error': f'Screenshot failed: {response.status_code}'},
                status=response.status_code,
            )

        return HttpResponse(
            response.content,
            content_type=response.headers.get('content-type', 'image/webp'),
            headers={'Cache-Control': 'public, max-age=3600'},
        )

Add to urls.py:

from django.urls import path
from .views import ScreenshotView

urlpatterns = [
    path('api/screenshot/', ScreenshotView.as_view(), name='screenshot'),
]

Django Cache Framework Integration

Cache screenshot responses to avoid redundant API calls:

# views.py
import hashlib
import requests
from django.http import HttpResponse, JsonResponse
from django.core.cache import cache
from django.views import View
from django.conf import settings


class CachedScreenshotView(View):
    CACHE_TTL = 3600  # 1 hour

    def get(self, request):
        target_url = request.GET.get('url')
        if not target_url:
            return JsonResponse({'error': 'Missing url parameter'}, status=400)

        fmt = request.GET.get('format', 'webp')
        width = request.GET.get('width', '1280')
        height = request.GET.get('height', '800')

        # Cache key based on parameters
        cache_key = 'screenshot:' + hashlib.sha256(
            f'{target_url}:{fmt}:{width}:{height}'.encode()
        ).hexdigest()[:16]

        cached = cache.get(cache_key)
        if cached:
            return HttpResponse(
                cached['data'],
                content_type=cached['content_type'],
                headers={'X-Cache': 'HIT'},
            )

        response = requests.get(
            f'{settings.SCREENSHOT_API_BASE}/api/screenshot',
            params={'url': target_url, 'format': fmt, 'width': width, 'height': height},
            headers={'X-API-Key': settings.SCREENSHOT_API_KEY},
            timeout=30,
        )

        if not response.ok:
            return JsonResponse({'error': 'Screenshot failed'}, status=502)

        content_type = response.headers.get('content-type', 'image/webp')
        cache.set(cache_key, {'data': response.content, 'content_type': content_type}, self.CACHE_TTL)

        return HttpResponse(response.content, content_type=content_type)

Celery Task for Async Screenshot Generation

For background processing — generating screenshots for reports, sending via email, or storing in S3:

# tasks.py
import requests
import boto3
from celery import shared_task
from django.conf import settings
from django.core.files.base import ContentFile
from .models import PagePreview


@shared_task(bind=True, max_retries=3, default_retry_delay=30)
def generate_screenshot(self, page_preview_id: int, target_url: str):
    """
    Generate a screenshot and save to the PagePreview model.
    Retries on transient failures.
    """
    try:
        response = requests.get(
            f'{settings.SCREENSHOT_API_BASE}/api/screenshot',
            params={'url': target_url, 'format': 'png', 'width': '1200', 'height': '630'},
            headers={'X-API-Key': settings.SCREENSHOT_API_KEY},
            timeout=30,
        )
        response.raise_for_status()

        preview = PagePreview.objects.get(id=page_preview_id)
        preview.screenshot.save(
            f'preview_{page_preview_id}.png',
            ContentFile(response.content),
            save=True,
        )
        preview.status = 'ready'
        preview.save()

    except requests.RequestException as exc:
        raise self.retry(exc=exc)
    except PagePreview.DoesNotExist:
        pass  # Record deleted before task ran — not an error

Trigger from a view or signal:

# views.py
from .tasks import generate_screenshot
from .models import PagePreview

def create_preview(request):
    url = request.POST.get('url')
    preview = PagePreview.objects.create(url=url, status='pending')
    generate_screenshot.delay(preview.id, url)  # runs in background
    return JsonResponse({'id': preview.id, 'status': 'pending'})

Django REST Framework Endpoint

For API-first Django projects using DRF:

# serializers.py
from rest_framework import serializers

class ScreenshotRequestSerializer(serializers.Serializer):
    url = serializers.URLField()
    format = serializers.ChoiceField(choices=['png', 'webp', 'jpeg'], default='webp')
    width = serializers.IntegerField(min_value=320, max_value=3840, default=1280)
    height = serializers.IntegerField(min_value=240, max_value=2160, default=800)
    full_page = serializers.BooleanField(default=False)
    delay = serializers.IntegerField(min_value=0, max_value=10000, default=0)
# views.py
import requests
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.http import HttpResponse
from django.conf import settings
from .serializers import ScreenshotRequestSerializer


class ScreenshotAPIView(APIView):
    """
    POST /api/screenshots/
    Returns screenshot image directly (content-type: image/*).
    """

    def post(self, request):
        serializer = ScreenshotRequestSerializer(data=request.data)
        if not serializer.is_valid():
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

        data = serializer.validated_data
        params = {
            'url': data['url'],
            'format': data['format'],
            'width': str(data['width']),
            'height': str(data['height']),
        }
        if data['full_page']:
            params['full_page'] = 'true'
        if data['delay']:
            params['delay'] = str(data['delay'])

        try:
            resp = requests.get(
                f'{settings.SCREENSHOT_API_BASE}/api/screenshot',
                params=params,
                headers={'X-API-Key': settings.SCREENSHOT_API_KEY},
                timeout=30,
            )
            resp.raise_for_status()
        except requests.HTTPError as e:
            return Response(
                {'error': f'Screenshot service error: {e.response.status_code}'},
                status=status.HTTP_502_BAD_GATEWAY,
            )
        except requests.RequestException:
            return Response(
                {'error': 'Screenshot service unavailable'},
                status=status.HTTP_503_SERVICE_UNAVAILABLE,
            )

        return HttpResponse(
            resp.content,
            content_type=resp.headers.get('content-type', 'image/webp'),
        )

Django Model + Admin Integration

Store screenshots alongside your content models:

# models.py
from django.db import models


class PageSnapshot(models.Model):
    STATUS_CHOICES = [
        ('pending', 'Pending'),
        ('ready', 'Ready'),
        ('failed', 'Failed'),
    ]

    url = models.URLField()
    screenshot = models.ImageField(upload_to='snapshots/', blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def capture(self):
        """Synchronous capture — use Celery task for production."""
        import requests
        from django.conf import settings
        from django.core.files.base import ContentFile

        resp = requests.get(
            f'{settings.SCREENSHOT_API_BASE}/api/screenshot',
            params={'url': self.url, 'format': 'png'},
            headers={'X-API-Key': settings.SCREENSHOT_API_KEY},
            timeout=30,
        )
        if resp.ok:
            self.screenshot.save(f'snapshot_{self.pk}.png', ContentFile(resp.content))
            self.status = 'ready'
            self.save()
        else:
            self.status = 'failed'
            self.save()

    class Meta:
        ordering = ['-created_at']
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import PageSnapshot


@admin.register(PageSnapshot)
class PageSnapshotAdmin(admin.ModelAdmin):
    list_display = ['url', 'status', 'thumbnail', 'created_at']
    readonly_fields = ['screenshot_preview']

    def thumbnail(self, obj):
        if obj.screenshot:
            return format_html(
                '<img src="{}" style="height:40px;border-radius:4px;" />',
                obj.screenshot.url,
            )
        return '-'

    def screenshot_preview(self, obj):
        if obj.screenshot:
            return format_html(
                '<img src="{}" style="max-width:800px;" />',
                obj.screenshot.url,
            )
        return 'No screenshot yet.'

Management Command for Batch Screenshots

A Django management command for generating or refreshing screenshots in bulk:

# management/commands/capture_snapshots.py
from django.core.management.base import BaseCommand
from django.conf import settings
import requests
import time


class Command(BaseCommand):
    help = 'Capture screenshots for all pending PageSnapshots'

    def add_arguments(self, parser):
        parser.add_argument('--force', action='store_true', help='Recapture even if already ready')
        parser.add_argument('--delay', type=float, default=0.5, help='Seconds between requests')

    def handle(self, *args, **options):
        from myapp.models import PageSnapshot

        qs = PageSnapshot.objects.all()
        if not options['force']:
            qs = qs.filter(status='pending')

        self.stdout.write(f'Processing {qs.count()} snapshots...')

        for snap in qs.iterator():
            try:
                snap.capture()
                self.stdout.write(f'  OK: {snap.url}')
            except Exception as e:
                self.stdout.write(self.style.ERROR(f'  FAIL: {snap.url} — {e}'))
            time.sleep(options['delay'])

        self.stdout.write(self.style.SUCCESS('Done.'))

Run with: python manage.py capture_snapshots --delay=1.0


Template Tag for Inline Screenshots

A custom template tag for use in Django templates:

# templatetags/screenshots.py
from django import template
from django.conf import settings
from django.utils.html import format_html
from urllib.parse import urlencode

register = template.Library()


@register.simple_tag
def screenshot_url(url, width=1280, height=800, fmt='webp'):
    """Return a URL to the screenshot proxy view."""
    params = urlencode({'url': url, 'format': fmt, 'width': width, 'height': height})
    return f'/api/screenshot/?{params}'


@register.inclusion_tag('screenshots/preview.html')
def page_preview(url, width=400, height=300):
    return {'url': url, 'width': width, 'height': height}
<!-- templates/screenshots/preview.html -->
<img
  src="{% screenshot_url url width height 'webp' %}"
  width="{{ width }}"
  height="{{ height }}"
  loading="lazy"
  alt="Preview of {{ url }}"
/>

Use in templates:

{% load screenshots %}
{% page_preview "https://example.com" 600 400 %}

Full API reference: /api. Get a free key (50/day): /api/keys.