diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c7bd26b7e8e94374e84f1d9c3252832610f6c2c5..e1c561ca6408c49b5d3fa3b084ce4b0e9818ec5f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -23,6 +23,8 @@ Changed
 * Rewrite of frontend using Vuetify
 * [Dev] Provide function to generate PDF files from fully-rendered templates.
 * OIDC scope "profile" now exposes the avatar instead of the official photo
+* [Dev] Accept pre-created file object for PDF generation to define
+  the redirect URL in advance.
 
 Fixed
 ~~~~~
diff --git a/aleksis/core/migrations/0042_pdffile_empty.py b/aleksis/core/migrations/0042_pdffile_empty.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8055132e11f2deba85ef4ecfa24840c51ce2081
--- /dev/null
+++ b/aleksis/core/migrations/0042_pdffile_empty.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.2.16 on 2022-11-03 11:36
+
+from django.db import migrations, models
+import django.utils.timezone
+import oauth2_provider.generators
+import oauth2_provider.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0041_update_gender_choices'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='pdffile',
+            name='html_file',
+            field=models.FileField(blank=True, null=True, upload_to='pdfs/', verbose_name='Generated HTML file'),
+        ),
+    ]
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 2f603ae0d2e2813b50f2024056cb460d71b4e484..0bf42033887664c79f71b0223deea994d69c21c5 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -1236,7 +1236,9 @@ class PDFFile(ExtensibleModel):
     expires_at = models.DateTimeField(
         verbose_name=_("File expires at"), default=_get_default_expiration
     )
-    html_file = models.FileField(upload_to="pdfs/", verbose_name=_("Generated HTML file"))
+    html_file = models.FileField(
+        upload_to="pdfs/", verbose_name=_("Generated HTML file"), blank=True, null=True
+    )
     file = models.FileField(
         upload_to="pdfs/", blank=True, null=True, verbose_name=_("Generated PDF file")
     )
diff --git a/aleksis/core/util/pdf.py b/aleksis/core/util/pdf.py
index 8721d8c3167ba8a01fd250a0967f56af495bdf78..c840c620fd102dc01b3300f2205e19e399b00ed2 100644
--- a/aleksis/core/util/pdf.py
+++ b/aleksis/core/util/pdf.py
@@ -88,10 +88,16 @@ def process_context_for_pdf(context: Optional[dict] = None, request: Optional[Ht
 
 
 def generate_pdf_from_html(
-    html: str, request: Optional[HttpRequest] = None
+    html: str, request: Optional[HttpRequest] = None, file_object: Optional[PDFFile] = None
 ) -> Tuple[PDFFile, AsyncResult]:
     """Start a PDF generation task and return the matching file object and Celery result."""
-    file_object = PDFFile.objects.create(html_file=ContentFile(html.encode(), name="source.html"))
+    html_file = ContentFile(html.encode(), name="source.html")
+
+    # In some cases, the file object is already created (to get a redirect URL for the PDF)
+    if not file_object:
+        file_object = PDFFile.objects.create()
+    file_object.html_file = html_file
+    file_object.save()
 
     # As this method may be run in background and there is no request available,
     # we have to use a predefined URL from settings then
@@ -110,6 +116,7 @@ def generate_pdf_from_template(
     context: Optional[dict] = None,
     request: Optional[HttpRequest] = None,
     render_method: Optional[Callable] = None,
+    file_object: Optional[PDFFile] = None,
 ) -> Tuple[PDFFile, AsyncResult]:
     """Start a PDF generation task and return the matching file object and Celery result."""
     processed_context = process_context_for_pdf(context, request)
@@ -119,7 +126,7 @@ def generate_pdf_from_template(
     else:
         html_template = render_to_string(template_name, processed_context, request)
 
-    return generate_pdf_from_html(html_template, request)
+    return generate_pdf_from_html(html_template, request, file_object=file_object)
 
 
 def render_pdf(