diff --git a/README.md b/README.md
index 5369054fdcf5a9ebea49bbdfaabcc0773e4c54d5..12bbc96e3e3852c433bcac731e771bfd94960d6d 100644
--- a/README.md
+++ b/README.md
@@ -15,65 +15,4 @@ service acts as an in-between layer and does support these features
 When visitors are registering to TNC2025 in Visit cloud, they are redirected to this service in the
 final step
 
-
-# Development
-
-## Config
-This service needs configuation data in the form of a config json file. By default, django look
-a file called `config.json` in the current directory. It is possble to override this by setting
-the environment variable `CONFIG_FILENAME` to point to a different location. A sample config is
-given in `config-example.json`. Make a copy named `config.json` and fill in the required data
-such as the `STRIPE_API_KEY` and the `VISIT.api-key` variables.
-
-## Database
-Connecting to a database can be done by setting the DATABASE_URL environment variable to a valid
-value:
-```
-export DATABASE_URL=postgresql://<username>:<password>@<host_address>/<database>
-```
-
-Altenatively, you can set up a local (sqlite) database. This can be done easily from the
-root of this repository
-
-```
-python manage-dev.py migrate
-```
-
-This will create a database file 'db.sqlite' in the current directory. You can then create a
-superuser account
-
-```
-python manage-dev.py createsuperuser
-```
-
-And finally you need to populate the database with price information. Sample price information
-is given in the `prices.json` file, but this file may not be completely up to date
-
-```
-python manage-dev.py createprices --file prices.json
-```
-
-## Development server
-You can then run the development server by running
-
-```
-python manage-dev.py runserver
-```
-
-## Admin interface
-The django admin interface is enabled, so when running the development server you can browse to
-`http://127.0.0.1:5000/admin` to login to the admin interface using the credentials you created
- using the createsuperuser (or other valid credentials if connected to an existing database). Here
- you can manage administrative users, priced items and other things.
-
-## Migrate price information to deployments
-
-with the correct enviroment settings setup, it is possble to dump the latest price information by running from the source database:
-```
-django-admin dumpdata stripe_checkout.PricedItem |json_pp > prices.json
-```
-
-to load price information into the target database, run:
-```
-django-admin loaddata prices.json
-```
+See the full documentation on [SWD Documentation](https://swd-documentation.geant.org/stripe-checkout/develop/)
diff --git a/docs/source/_images/visit-bridgeserviceurl.png b/docs/source/_images/visit-bridgeserviceurl.png
new file mode 100644
index 0000000000000000000000000000000000000000..333b7021b83b91237dae2db566eeccfa61389ab7
Binary files /dev/null and b/docs/source/_images/visit-bridgeserviceurl.png differ
diff --git a/docs/source/_images/visit-form-redirect.png b/docs/source/_images/visit-form-redirect.png
new file mode 100644
index 0000000000000000000000000000000000000000..13ca791c62211c557d7aac9a229bc2b74b5cebad
Binary files /dev/null and b/docs/source/_images/visit-form-redirect.png differ
diff --git a/docs/source/_static/.gitkeep b/docs/source/_static/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/docs/source/administration.rst b/docs/source/administration.rst
new file mode 100644
index 0000000000000000000000000000000000000000..ab74dcda3d3508e212a081b91c17810ea2999c4c
--- /dev/null
+++ b/docs/source/administration.rst
@@ -0,0 +1,104 @@
+.. _administation:
+
+Administration
+==============
+
+For administration, log in from the root url ``/`` in the stripe checkout server. Credentials are
+available in Lastpass. The following pages are available when logged in:
+
+.. _django-admin:
+
+Django Admin portal (Admin)
+---------------------------
+
+.. _manage-users:
+
+Manage Users
+############
+
+You can add new users that can login and manage certain aspects of the website. Based on what the
+user should be able to do, they need the following permissions:
+
+* Log in: Staff status (Basically every user should have this)
+* Access to django admin: Superuser status
+* Manage visitors: ``stripe_checkout | order | Manage visitors``
+* Download Stripe Report: ``stripe_checkout | report | Can view report``
+
+.. _adding-product-info:
+
+Adding new products or price information to the database
+########################################################
+
+Log in to the django admin console and add or update a ``PricedItem``. There are two different kinds
+of ``PricedItem``: ``Registration Type`` and ``Extra``
+
+``Registration Type`` items are used to link a visitor's ``registrationType.id`` to a Stripe product
+id. Required fields are
+
+* ``Stripe Product id``: The stripe product for this item. Stripe product ids start with ``prod_``
+* ``Visit registration id``: The visit ``registrationType.id`` for this registration type. See also
+  "New registration types" below
+
+``Extra`` items are tied to specific questions and answers in the Visit. A visitor may opt in
+(purchase) zero or more extras during their registration, and possibly after. Every extra requires:
+
+* ``Visit checkout question id`` and ``Visit checkout answer id``: a combination of a question and
+  answer in visit that indicates that the visitor wants to purchase this extra.
+
+* ``Visit paid question id`` and ``Visit paid answer id``: a combination of a question and
+  answer in visit to indicdate that the visitor has paid for this extra. Upon receiving confirmation
+  of payment, stripe checkout will set this combination in the visitor's profile.
+
+Aside from the fields described above, the following fields are required for all ``PricedItem``
+objects:
+
+* ``Name``: a snake case name that helps with idenfication. This name is not shown to visitors
+* ``Display Name``: a user friendly name. This name is shown to visitors in their checkout page and
+  should match the user facing name in Visit and Stripe
+* ``Kind``: See above
+
+Migrate price information between deployments
+#############################################
+
+With the correct enviroment settings setup, it is possble to dump the latest price information by
+running from the source database::
+
+  django-admin dumpdata stripe_checkout.PricedItem | json_pp > prices.json
+
+To load price information into the target database, change your database settings and run::
+
+  django-admin loaddata prices.json
+
+
+GBP Vat
+#######
+
+GBP VAT rates are stored in the Exchange rates section. This is done automatically by the
+:ref:`getexchangerate<getexchangerate>` management command that is run periodically.
+
+.. _manage_visitors:
+
+
+Manage Visitors
+---------------
+
+A special page is made to quickly manage visitors (``/visitors``). Here it is possible to change
+what a Visitor has checked out and paid for. it is also possible to clear any ``Order``\s that have
+been created for the visitor. This is useful during development and testing, but should not be used
+in production. Due to limitations of the Visit API, this page only shows the first 100 registered
+visitors.
+
+.. warning::
+
+  Be careful, it is very easy to accidentally make a mistake when editing data through the
+  ``/visitors`` endpoint. This may cause visitors to be registered incorrectly and to have an
+  incorrect payment status
+
+.. _download_stripe_report:
+
+Download Stripe Report
+----------------------
+
+Finance has requested a (periodic) report of all invoices and credit notes in Stripe. Stripe does
+not offer a reporting functionality sufficient for Finance, so every day a report is generated
+in a csv format using the ``generatereport`` management command.
\ No newline at end of file
diff --git a/docs/source/conf.py b/docs/source/conf.py
index d1d36296d264a460a5dea85adedb26954d2c0d1b..ae7f6ae6c7593732fac9eeb125de7d302493955c 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -58,7 +58,7 @@ def setup(app):
 
 # TODO: give this a better project name
 project = "stripe-checkout"
-copyright = "2022, GÉANT Vereniging"
+copyright = "2025, GÉANT Vereniging"
 author = "swd@geant.org"
 
 # The full version, including alpha/beta/rc tags
diff --git a/docs/source/development.rst b/docs/source/development.rst
new file mode 100644
index 0000000000000000000000000000000000000000..78efeab1cd0425fea4e24a81e432e70a92cee0ca
--- /dev/null
+++ b/docs/source/development.rst
@@ -0,0 +1,194 @@
+Development
+===========
+
+.. _installation:
+
+Installation
+------------
+
+Install the package and dependencies::
+
+  pip install -r requirements.txt
+  pipe install -e .
+
+Create a ``config.json`` file::
+
+  cp config-example.json config.json
+
+Fill out the ``STRIPE_API_KEY`` and ``VISIT_API_KEY`` settings.
+
+Setup a local (sqlite) database::
+
+  python manage-dev.py migrate
+  python manage-dev.py createsuperuser
+  python manage-dev.py loaddata prices-test.json
+
+This will create a file ``db.sqlite`` and initialize it with the necessary tables and data. You
+can now run a development server by running::
+
+  python manage-dev.py runserver
+
+.. _invoking-management-commands:
+
+Invoking management commands
+----------------------------
+The ``manage-dev.py`` file is an entrypoint to running django management commands. The generic CLI
+tool for running
+`django-admin<https://docs.djangoproject.com/en/4.2/ref/django-admin/>`
+This command requires at least one enviroment variable to be set ``DJANGO_SETTINGS_MODULE`` to
+point to a python module for the project's settings as a dotted path. For local development, the
+path should be ``stripe_checkout.settings.dev``. Inside the root of the ``stripe_checkout`` repository,
+The ``manage-dev.py`` file is preconfigured with this enviroment variable, and a ``CONFIG_FILENAME``
+environment variable pointing to the ``config.json`` file created during :ref:`installation`.
+
+When invoking management commands in the deployed VMs you should use the ``/home/tnc/cmd.sh``
+script instead of ``django-admin``. This script is configured with the correct environment variables
+for running django commands against the deployed instance.
+
+
+Settings
+--------
+
+There are three ways of supplying settings. These are described below
+
+
+Settings file
+#############
+
+For development, use the ``stripe_checkout.settings.dev`` settings module. Deployed environments
+(test, uat, prod) use the ``stripe_checkout.settings.prod`` settings module. These files contain
+settings that are static between the environments, and contain some logic for reading and parsing
+environment variables that are set for specific environments.
+
+
+
+.. _config_json:
+
+
+Config file
+###########
+
+The config file is a way to supply settings that are non-static, and therefore cannot be placed
+in a settings file. The config file must be a json file containing a top level dictionary, where
+the keys are settings (are therefore must be ``UPPER_CASE``) and are directly interpreted as django
+settings. This allows the values to have an arbitrary structure other than a single string, such
+as an array. The config file is validated to a json schema, located in ``stripe_checkout.config``.
+In order to use a specific config file, set the ``CONFIG_FILENAME`` environment variable to a file
+path ``path/to/config.json`` that points to a valid config file. This can be an absolute path or
+a relative path. For discoverability, it is better to use an absolute path.
+
+
+Environment Variables
+#####################
+
+We use the ``DJANGO_SETTINGS_MODULE`` and ``CONFIG_FILENAME`` to point to the settings file
+``dotted.path`` and config file ``path/to/config.json``. There are also other environment variables
+for settings that cannot be static in a settings file, because they either vary between environments,
+or contain sensitive information. The settings files are set up in a way to read certain enviroment
+variables and use them for specific settings.
+
+.. note::
+  Both the ``config.json`` and environment variables can be used for variable settings. When to use one
+  over the other is somewhat arbitrary, but there are guidelines. Settings that are relevant to the
+  application specifics should be placed in the ``config.json``, such as api keys and specific
+  stripe or visit ids. Also settings that have a more complex structure than a single string (such
+  as an array) are more easily set using a ``config.json`` Settings that have a more
+  infrastructure-like nature, or have to do with django internals, such as ``DJANGO_FRONTEND_URL``
+  or ``DJANGO_SECRET_KEY`` are supplied using environment variables. Another reason to use an
+  environment variable is when a value needs to be processed before being assigned to the actual
+  setting, as is the case with the ``DATABASE_URL`` environment variable.
+
+
+Database
+--------
+
+Stripe checkout needs a database to function properly. In development this is a sqlite database.
+You can also connect to the test database by setting the ``DATABASE_URL`` environment variable
+(substitute the username and password with the data in Lastpass)::
+
+  export DATABASE_URL=postgres://<username>:<password>@test-postgres.geant.org/stripe_checkout
+
+
+If you now run ``django-admin`` or ``manage-dev.py`` this will connect to the database for the
+test deployment.
+
+
+Email
+-----
+``stripe-checkout`` sends an email when it sees a succeeded payment intent that doesn't match an
+``Order`` in the database, and therefore can't link it to a visitor's purchase. It uses Django's
+`send_mail` functionality. Django uses the following settings:
+
+- `EMAIL_HOST` (default: `localhost`)
+- `EMAIL_PORT` (default: `25`)
+
+It so happens postfix is configured on the ``stripe-checkout`` vm's to forward to the geant smtp
+servers. So we don't need much additional configuration. We only configure the `DEFAULT_FROM_EMAIL`
+setting. The recipients for these emails can be set using the ``UNPROCESSED_PAYMENT_EMAIL_TO``
+setting in the :ref:`config_json`
+
+.. _stripe_webhook_endpoint:
+
+Stripe webhook endpoint
+-----------------------
+
+Stripe can be configured to call a `webhook endpoint <https://docs.stripe.com/webhooks>`_ for certain
+Stripe events. These webhooks can be configured in the `Stripe Workbench <https://dashboard.stripe.com/workbench/webhooks>`_.
+Stripe is configured to call the ``/stripe-event-webhook/`` endpoint for ``payment_intent.succeeded``
+and ``payment_intent.canceled`` webhooks (see also: :ref:`design`). There may be other events
+configured as well, but these are ignored when :ref:`processing events <processevents>`.
+
+During development it is possible to register your locally running development server as an event
+webhook using the `stripe cli <https://docs.stripe.com/stripe-cli>`_ and register a
+`local listener <https://docs.stripe.com/webhooks#local-listener>`_. After setting up stripe cli and
+logging in, you can hook up the local developer server with the following command::
+
+  stripe listen --events payment_intent.succeeded,payment_intent.canceled \
+    --forward-to 127.0.0.1:8000/stripe-event-webhook/
+
+Upon running this command, the stripe cli will output a stripe signing secret. This is a secret key
+that stripe will use to sign the event payload so that we can be certain the events are actually
+coming from Stripe. Add this secret to you ``config.json`` as the ``STRIPE_SIGNING_SECRET`` setting
+and restart the development server.
+
+``Events``\s will now be registered in the local database. Run the following command to process these
+events::
+
+  python manage-dev.py processevents
+
+.. note::
+  One way the ``/stripe-event-webhook`` is secured is through the signing secret. The second way
+  this endpoint is secured, is that only IP addresses from stripe can actually call this endpoint.
+  The full list of stripe webhook IP addresses can be found
+  `here <https://docs.stripe.com/ips#webhook-notifications>`_. These IP addresses are placed in the
+  ``STRIPE_WEBHOOK_ALLOWED_IPS`` setting in the ``stripe_checkout.settings.prod`` settings module.
+
+.. _visit-redirect:
+
+Visit redirect
+--------------
+The redirect to the stripe checkout server in Visit is done using a custom javascript snippet:
+
+1. Click the final page "Complete"
+2. Click "current page settings"
+3. Clikc "Edit Javascript"
+
+.. image:: _images/visit-form-redirect.png
+  :alt: Visit form redirect instructions
+
+Redirect URL
+############
+
+The redirect url is stored in the Visit custom field ``bridgeserviceurl``. To edit this, login to
+Visit, open the TNC 2025 event and follow these steps:
+
+1. Click on Event
+2. Click on Setup
+3. Click on Custom fields
+
+
+.. image:: _images/visit-bridgeserviceurl.png
+  :width: 700
+  :alt: Visit Redirect URL
+
+For more information on custom fields, see the Visit `documentation <https://help.visitcloud.com/create/docs/user-guide/organisation/edit-the-organisation/#add-and-edit-custom-fields>`_
\ No newline at end of file
diff --git a/docs/source/endpoints.rst b/docs/source/endpoints.rst
new file mode 100644
index 0000000000000000000000000000000000000000..2333bb6e56a42c65dde8d4897a522a02917ae4d4
--- /dev/null
+++ b/docs/source/endpoints.rst
@@ -0,0 +1,48 @@
+Endpoints
+=========
+
+Visitor endpoints
+-----------------
+The following endpoints are configured in this service
+
+
+``/checkout/<visitor_id>/``
+###########################
+
+The main entry point for users. Here they can configure a PO and VAT number and confirm their
+order. Upon confirmation they get redirected to Stripe.
+
+
+
+``/checkout/stripe-event-webhook/``
+###################################
+
+This endpoint is configured as a stripe event webhook, so that we get updated whenever
+a payment (credit card or bank transfer) has succeeded and we can update the visitors Paid
+status in Visit. It is secured using `Stripes signing method
+<https://docs.stripe.com/webhooks#verify-official-libraries>`_. See also
+:ref:`stripe_webhook_endpoint`
+
+
+
+Administrative endpoints
+------------------------
+``/admin/``
+###########
+
+Access to the Django admin interface. Here you can add new users, and manage PricedItems and
+Orders. See :ref:`django-admin`
+
+
+``/visitors/``
+##############
+
+Allow for quickly setting a visitor's registration type, extras and paid status in Visit. See 
+:ref:`manage_visitors`
+
+
+``/report/``
+##############
+
+Download a daily generated report with all invoices and credit notes in Stripe. See
+:ref:`download_stripe_report`
diff --git a/docs/source/gateway.rst b/docs/source/gateway.rst
deleted file mode 100644
index a6c2d43ba1b716ad62e1af013a6413dc1fe84794..0000000000000000000000000000000000000000
--- a/docs/source/gateway.rst
+++ /dev/null
@@ -1,84 +0,0 @@
-.. gateway intro
-
-Visit Stripe Gateway
-====================
-
-This service acts as a gateway between Visit(cloud) and Stripe and its main purpose is to be able
-to customize the checkout process for people registering for TNC2025 and enable the following
-features:
-
- * Payment through bank transfer
- * Addition of PO and VAT number in the invoice
-
-These features are not supported by Visit's implementation of the Stripe integration. This
-service acts as an in-between layer and does support these features
-
-When visitors are registering to TNC2025 in Visit cloud, they are redirected to this service in the
-final step of the registration form. Here they get the opportunity to fill out a purchase order
-and/or VAT number and select wether to pay using credit card or bank transfer. They then get
-redirected to the relevant stripe web page
-
-Design
-------
-This gateway service is designed in two parts. The first part is a Django app that takes care
-of user interaction, setting up the Stripe payment objects and redirecting the users. When it
-creates a payment object (either a checkout Session or a PaymentIntent), it also creates a database
-entry (name ``Order``) that links the stripe payment object back to the Visit visitor (ie. the
-registrant). It does not directly update the visitor's paid status in Visit. Whenever the visitor
-has fulfilled its payment, Stripe will call a webhook in our service (see below) to notify us of
-this fact. We store this ``Event`` in our database and process it asynchronously. Stripe requires
-us to handle the webhook request fast, so we cannot directly update the status in Visit.
-
-The second part of the design is a worker script that checks the databases for any new ``Event``
-entries, and performs the necessary updates in our database and in Visit through the Visit API.
-This worker is configured as a cronjob
-
-
-
-Endpoints
----------
-The following endpoints are configured in this service
-
-
-``/checkout/<visitor_id>/``
-###########################
-
-The main entry point for users. Here they can configure a PO and VAT number and confirm their
-order. Upon confirmation they get redirected to Stripe.
-
-``/checkout/<visitor_id>/success/``
-###################################
-
-Used as a redirect by stripe on a successful payment. Because this endpoint not being secure, we
-do not update the Visit paid status.
-
-``/checkout/<visitor_id>/cancel/``
-##################################
-
-Used as a redirect by stripe on a canceled payment
-
-``/checkout/stripe-event``
-##########################
-
-This endpoint is configured as a stripe event webhook, so that we get updated whenever
-a payment (credit card or bank transfer) has succeeded and we can update the visitors Paid
-status in Visit. It is secured using Stripes signing secret method (cf.
-`https://docs.stripe.com/webhooks#verify-official-libraries`_
-)
-
-
-
-Administrative endpoints
-------------------------
-
-/visitors/
-##########
-
-Allow for quickly setting a visitor's shopping cart and paid items in Visit.
-
-/admin/
-#######
-
-Access to the Django admin interface. Here you can add new users, and manage PricedItems and
-Orders
-
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 18e0e51d284e9b1758eae7c82a4e9cb1e0c7c65a..e51ecc4bf0227698e0a09a1b8b598439dbea964d 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -1,9 +1,11 @@
 stripe_checkout
 ========================================
 
-Placeholder for docs
 .. toctree::
    :maxdepth: 2
    :caption: Contents:
 
-   gateway
+   introduction
+   endpoints
+   administration
+   development
\ No newline at end of file
diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst
new file mode 100644
index 0000000000000000000000000000000000000000..e87e523e78b6b8ecdbbf40bd4fac6d6fa597319e
--- /dev/null
+++ b/docs/source/introduction.rst
@@ -0,0 +1,150 @@
+.. _introduction:
+
+Introduction
+============
+
+This service acts as a gateway between Visit(cloud) and Stripe and its main purpose is to be able
+to customize the checkout process for people registering for TNC2025 and enable the following
+features:
+
+* Payment through bank transfer or credit card
+* Customization of the invoice
+
+  * allow the visitors to set a PO number and VAT number
+  * add the VAT in GBP as well as EUR
+  * Use a specific invoice template
+
+These features are not supported by Visit's implementation of the Stripe integration. This
+service acts as an in-between layer and does support these features
+
+When visitors are registering to TNC2025 in Visit cloud, they are redirected to this service in the
+final step of the registration form. Here they get the opportunity to fill out a purchase order
+and/or VAT number. Upon clicking a confirmation button an invoice is created for them and they are
+redirected to Stripe where they can view the invoice, pay directly by credit card and/or see
+payment instructions for paying by bank transfer. They also receive an e-mail with all payment
+information
+``
+
+
+.. note::
+  Throughout this documentation, there are instructions to invoke django management commands. These
+  instructions use the ``django-admin`` cli tool to invoke these commands. Depending on the
+  situation you may want to use another script to invoke these commands, such as ``manage-dev.py``
+  or ``cmd.sh``. See also :ref:`invoking-management-commands`
+
+
+.. _design:
+
+Design
+------
+
+The design of the stripe-checkout service is roughly as following:
+
+Whenever a visitor registers for TNC, they fill out a form in Visit. This form sets their
+  ``registrationType``. While filling out the form, the visitor may also want to purchase some extras,
+  such as access to side meetings or social events. The information for extras is stored in the
+  visitors profile as answers to certain questions. For example, there is a question for "Side
+  meetings" with answers "Friday" and "Sunday"
+
+* As the final page in the registration form, the visitor is redirected to the stripe checkout
+  service. Here they see what they've checked out and how much they need to pay.
+
+  * The checkout service reads the visitor's profile through the Visit API (the visitor is
+    identified by their visitor id in the url).
+  * From the visitor's profile, the Stripe products that the visitor needs to pay for are
+    determined. A visitor can have a single Registration Type item and multiple extras. Every
+    category of extras has two questions: One that the visitor answers with the extras they want,
+    and one that is goverened by the stripe checkout service when the visitor's payment has been
+    processed. Every answer in the paid-questions represents an extra that the visitor has paid for.
+
+    These are defined as ``PricedItem`` objects in the database. See also: :ref:`adding-product-info`
+  * All items that the visitor has not previously purchased (see ``Order`` below) are added to a
+    ``ShoppingCart`` and this collection is shown to the user in their checkout page
+
+* In their checkout page, the visitor can add some additional information that will be shown on
+  the invoice. These are a Purchase Order (PO) number and/or a VAT id. These fields are optional.
+  Then the visitor clicks Confirm to finalize the purchase
+* The stripe checkout service now creates an invoice in Stripe for the items that they need to pay
+  for:
+
+  * All ``PricedItem`` objects that the visitor needs to pay for are collected in an ``Order`` and
+    stored in the database. Any items for which the visitor already has an ``Order`` are excluded from
+    the new ``Order``, so that the visitor is charged only once for an item.
+  * The visitor's billing address is read from their Visit profile. ``contact.addresses`` should
+    contain an address object of ``"type": "billing"``.
+
+    .. note::
+
+      In order for any billing information to be
+      shown in the Invoice, it is required that the ``country`` field is set to a valid country
+      code in the billing address.
+
+  * The visitor is identified in stripe by their email address. If the visitor's email address
+    is already bound to a customer in Stripe, we use that customer, but update the billing details
+    using their Visit profile
+  * Additional fields are added to the the invoice as ``custom_fields``. These are:
+
+    * Purchase Order: Supplied by the visitor (optional)
+    * VAT number: Supplied by the visitor (optional)
+    * GBP VAT:  The VAT amount in GBP. The exchange rate read from the database and is periodically
+      updated by management command ``getexchangerate``
+
+  * We store the ``visitor_id`` and ``order_id`` as metadata fields to the invoice. That way we can
+    always find back the order if we have the Stripe invoice.
+  * We tell Stripe to finalize and send the invoice by email to the visitor. Stripe also generates
+    a url where the visitor can directly pay their invoice. We redirect the user to this url. The
+    visitor facing part of the stripe checkout service is now finished.
+  * When creating and finalizing the Invoice, Stripe also creates a payment intent. Before
+    redirecting the visitor to their Stripe payment page, we store the payment intent's id
+    (starting with ``pi_``) along with the ``Order`` so that we can reference it later.
+
+* We now wait for the visitor to pay. The visitor may pay by credit card or by bank transfer. While
+  credit card payments are processed almost instantly, bank transfers may take time to be
+  processed. First, the visitor needs to actually pay, and then Stripe needs to process the
+  payment. The total time between placing the order and strip processing the payment may take weeks
+  or longer.
+* When the visitor's payment has been succesfully processed by Stripe, Stripe calls our webhook at
+  the ``stripe-event-webhook/`` endpoint. This endpoint does not process the event, but merely stores
+  it in the database as an Event. Stripe requires that this endpoint returns quickly so processing
+  the events happens asynchronously by periodically running the django managemement command
+  ``processevents``
+
+.. _processevents:
+
+Processing stripe events
+########################
+
+* The ``processevents`` command looks in the database for Events that are not processed yet. It
+  looks at two types of events: ``payment_intent.succeeded`` and ``payment_intent.canceled``.
+
+  * ``payment_intent.succeeded`` events indicate that the visitor has paid. We look in the ``Order``
+    table for a matching ``Order``, and update the visitor's Profile in Visit. We add the ``PAID``
+    tag to the visitor to indicate that they've paid for their main registration, and add the relevant
+    answers to the "paid"-question for extra's. In certain cases there is no ``Order`` that is
+    linked to the payment_intent. This can happen in the following circumstances and we handle
+    these differently
+
+    * If during processing of the invoice, the invoice (and payment intent) gets created, but for
+      some reason we are unable to write back the payment intent id to the ``Order``, we end up
+      with an ``Order`` without a payment intent reference. In this case we do a reverse lookup. We
+      retrieve the invoice from the payment intent and look in the metadata for a ``visitor_id`` and
+      and ``order_id``. If we can find an order that match those parameters we use it and continue
+      as normal
+    * If the original invoice was credited and another invoice was created manually, this has a new
+      payment intent attached to it. The ``Order`` now has an incorrect payment intent reference.
+      We try the reverse lookup using the invoice's metadata, but if that fails, we send an email
+      with a notification that we encountered a payment intent that we cannot process. The
+      payment_intent / invocie must be processed manually
+
+  * ``payment_intent.canceled`` events happen when an invoice was credited. We add the
+    ``CANCELLED`` tag to the visitor, to indicate that we now no longer expect them to pay
+
+.. _getexchangerate:
+
+Fetching exchange rates
+#######################
+* Another job that runs periodically is the ``getexchangerate`` command. Every day it looks for a
+  new GBP/EUR  exchange rate on
+  `https://www.trade-tariff.service.gov.uk/exchange_rates <https://www.trade-tariff.service.gov.uk/exchange_rates>`_
+  and if it finds one, adds it to the database to be used from now on. The trade-tariff service
+  publishes the exchange rates every month.
diff --git a/prices-live.json b/prices-live.json
index 111c7b5a90f39152f76bd964e9bfae4ab6919f6f..a5652dc1b3e3e35c341d81e50e9ae3db6cd295f7 100644
--- a/prices-live.json
+++ b/prices-live.json
@@ -49,7 +49,7 @@
          "display_name" : "Side meeting (Friday)",
          "kind" : "EXTRA",
          "name" : "side_meeting_pass_friday",
-         "stripe_product_id" : "prod_RZw4eSzXLC1A2N",
+         "stripe_product_id" : "prod_RZw4cMh6B1tL3T",
          "visit_checkout_answer_id" : "1zeb3k6yy",
          "visit_checkout_question_id" : "1zeb2vc8p",
          "visit_paid_answer_id" : "1zen12x8z",
@@ -64,7 +64,7 @@
          "display_name" : "Side meeting (Monday)",
          "kind" : "EXTRA",
          "name" : "side_meeting_pass_monday",
-         "stripe_product_id" : "prod_RPrt4MzT2m98d2",
+         "stripe_product_id" : "prod_RZw4eSzXLC1A2N",
          "visit_checkout_answer_id" : "1zeb3k6yx",
          "visit_checkout_question_id" : "1zeb2vc8p",
          "visit_paid_answer_id" : "1zen12x8y",
@@ -118,5 +118,20 @@
       },
       "model" : "stripe_checkout.priceditem",
       "pk" : 9
+   },
+   {
+      "fields" : {
+         "display_name" : "PC Pass",
+         "kind" : "REGISTRATION_TYPE",
+         "name" : "pc_pass",
+         "stripe_product_id" : "prod_Ria6hn9u7weTad",
+         "visit_checkout_answer_id" : "",
+         "visit_checkout_question_id" : "",
+         "visit_paid_answer_id" : "",
+         "visit_paid_question_id" : "",
+         "visit_registration_id" : "1zeurvsr3"
+      },
+      "model" : "stripe_checkout.priceditem",
+      "pk" : 10
    }
 ]
diff --git a/setup.py b/setup.py
index 9caed8a00265d611494318df2fe077f549b6aecd..081cced6759ff1beacb6c2241045174aec6838b2 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
 
 setup(
     name="stripe-checkout",
-    version="0.9",
+    version="0.10",
     author="GEANT",
     author_email="swd@geant.org",
     description="Stripe custom checkout support service",
diff --git a/stripe_checkout/config.py b/stripe_checkout/config.py
index 94400c3a28f28cbfd11689945dd9acc614b53fc1..bda88c5a1e815a5632721af44862a354c4d25974 100644
--- a/stripe_checkout/config.py
+++ b/stripe_checkout/config.py
@@ -13,6 +13,10 @@ CONFIG_SCHEMA = {
         "STRIPE_TAX_RATE_ID": {"type": "string"},
         "VISIT_API_KEY": {"type": "string"},
         "VISIT_EXPO_ID": {"type": "string"},
+        "UNPROCESSED_PAYMENT_EMAIL_TO": {
+            "type": "array",
+            "items": {"type": "string"},
+        },
     },
     "required": [
         "STRIPE_API_KEY",
diff --git a/stripe_checkout/stripe_checkout/management_views.py b/stripe_checkout/stripe_checkout/management_views.py
index 5329256c8126df989e0a46051de017c58cdd8eb2..9639f3b7aaf77ae47a70c8702cb0a9c7bfff1dc2 100644
--- a/stripe_checkout/stripe_checkout/management_views.py
+++ b/stripe_checkout/stripe_checkout/management_views.py
@@ -1,7 +1,7 @@
 from typing import Sequence
 from django.http import Http404, HttpResponse
 from django.shortcuts import redirect, render
-from django.contrib.auth.decorators import login_required
+from django.contrib.auth.decorators import login_required, permission_required
 
 from stripe_checkout.stripe_checkout.models import ItemKind, Order, PricedItem, Report
 from . import visit
@@ -14,6 +14,7 @@ def index(request):
 
 
 @login_required
+@permission_required("stripe_checkout.manage_visitors")
 @require_GET
 def list_visitors(request):
     api = visit.VisitorAPI()
@@ -37,6 +38,7 @@ def list_visitors(request):
 
 
 @login_required
+@permission_required("stripe_checkout.manage_visitors")
 @require_POST
 def update_visitor(request, visitor_id):
     all_extras: Sequence[PricedItem] = (
@@ -65,6 +67,7 @@ def update_visitor(request, visitor_id):
 
 
 @login_required
+@permission_required("stripe_checkout.manage_visitors")
 @require_POST
 def delete_orders(request, visitor_id):
     Order.objects.filter(visitor_id=visitor_id).delete()
@@ -72,6 +75,7 @@ def delete_orders(request, visitor_id):
 
 
 @login_required
+@permission_required("stripe_checkout.view_report")
 @require_GET
 def get_report(request):
     report = Report.objects.first()
diff --git a/stripe_checkout/stripe_checkout/migrations/0009_alter_order_options.py b/stripe_checkout/stripe_checkout/migrations/0009_alter_order_options.py
new file mode 100644
index 0000000000000000000000000000000000000000..e27d1b20bdb17a8326694b06688d2781e2c83afa
--- /dev/null
+++ b/stripe_checkout/stripe_checkout/migrations/0009_alter_order_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.18 on 2025-02-13 14:27
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("stripe_checkout", "0008_order_created_alter_order_stripe_id"),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name="order",
+            options={"permissions": (("manage_visitors", "Manage visitors"),)},
+        ),
+    ]
diff --git a/stripe_checkout/stripe_checkout/models.py b/stripe_checkout/stripe_checkout/models.py
index 3bbd90adda9732c33d0eda4981157ace681331cf..2276ef9c773ef157b066570d286ecf4bfdadf2da 100644
--- a/stripe_checkout/stripe_checkout/models.py
+++ b/stripe_checkout/stripe_checkout/models.py
@@ -83,6 +83,9 @@ class Order(models.Model):
     )
     created = models.DateTimeField(auto_now_add=True)
 
+    class Meta:
+        permissions = (("manage_visitors", "Manage visitors"),)
+
 
 class Event(models.Model):
     payload = models.JSONField(blank=False)
diff --git a/stripe_checkout/stripe_checkout/templates/index.html b/stripe_checkout/stripe_checkout/templates/index.html
index d5baaf75745acc9ee62f95e6c9d7275352ed59c7..69b22e3ba73a00b0ad8fbbc68a40b12903f52e9f 100644
--- a/stripe_checkout/stripe_checkout/templates/index.html
+++ b/stripe_checkout/stripe_checkout/templates/index.html
@@ -4,14 +4,20 @@
 {% endblock title %}
 {% block body %}
     <ul>
-        <li>
-            <a href="{% url 'admin:index' %}">Admin</a>
-        </li>
-        <li>
-            <a href="{% url 'stripe_checkout:list-visitors' %}">Manage visitors</a>
-        </li>
-        <li>
-            <a href="{% url 'stripe_checkout:get-report' %}">Download Stripe Report</a>
-        </li>
+        {% if user.is_superuser %}
+            <li>
+                <a href="{% url 'admin:index' %}">Admin</a>
+            </li>
+        {% endif %}
+        {% if perms.stripe_checkout.manage_visitors %}
+            <li>
+                <a href="{% url 'stripe_checkout:list-visitors' %}">Manage visitors</a>
+            </li>
+        {% endif %}
+        {% if perms.stripe_checkout.view_report %}
+            <li>
+                <a href="{% url 'stripe_checkout:get-report' %}">Download Stripe Report</a>
+            </li>
+        {% endif %}
     </ul>
 {% endblock body %}
diff --git a/stripe_checkout/stripe_checkout/templates/invoice-created.html b/stripe_checkout/stripe_checkout/templates/invoice-created.html
deleted file mode 100644
index 6fd681a95162a8a6dee33a432e70621cccf090a3..0000000000000000000000000000000000000000
--- a/stripe_checkout/stripe_checkout/templates/invoice-created.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{% extends "tnc-base.html" %}
-{% block title %}
-    Invoice sent
-{% endblock title %}
-{% block content %}
-    <div class="checkout flex center">
-        <div class="card flex center column">
-            <p>
-                Thank you for registering for TNC 2025! You should soon receive an email with an
-                invoice for your registration as well as payment instructions. If not, please check
-                your junk mail. After you have successfully paid the invoice, your registration
-                will be completed.
-            </p>
-        </div>
-    </div>
-{% endblock content %}
diff --git a/stripe_checkout/stripe_checkout/templates/visitors.html b/stripe_checkout/stripe_checkout/templates/visitors.html
index 2376d6a6d1fb4f013b858646cbc104a57799257d..9985eba966070cdb7a0a9c948450630ee8779af5 100644
--- a/stripe_checkout/stripe_checkout/templates/visitors.html
+++ b/stripe_checkout/stripe_checkout/templates/visitors.html
@@ -21,7 +21,7 @@
                 {% for visitor in visitors %}
                     <tr>
                         <td>
-                            <a href="{% url 'stripe_checkout:checkout-entrypoint' visitor.id %}"
+                            <a href="{% url 'stripe_checkout:checkout' visitor.id %}"
                                target="_blank">{{ visitor.id }}</a>
                         </td>
                         <td>{{ visitor.contact.firstName }} {{ visitor.contact.lastName }}</td>
diff --git a/stripe_checkout/stripe_checkout/urls.py b/stripe_checkout/stripe_checkout/urls.py
index dffd24314a944f020440b36aee085211df144525..373752eea232b71d6ed69d911decfef943ec8818 100644
--- a/stripe_checkout/stripe_checkout/urls.py
+++ b/stripe_checkout/stripe_checkout/urls.py
@@ -7,7 +7,7 @@ from .management_views import (
     list_visitors,
     update_visitor,
 )
-from .visit_views import checkout, checkout_success, stripe_event
+from .visit_views import checkout, stripe_event
 
 visitor_id = r"(?P<visitor_id>[a-z0-9]+)"
 
@@ -26,15 +26,7 @@ urlpatterns = [
         ),
     ),
     path("report/", get_report, name="get-report"),
-    re_path(
-        f"checkout/{visitor_id}/",
-        include(
-            [
-                path("success/", checkout_success, name="checkout-success"),
-                path("", checkout, name="checkout-entrypoint"),
-            ]
-        ),
-    ),
+    re_path(f"checkout/{visitor_id}/", checkout, name="checkout"),
     path("stripe-event-webhook/", stripe_event, name="stripe-event"),
     path("", index, name="index"),
 ]
diff --git a/stripe_checkout/stripe_checkout/visit_views.py b/stripe_checkout/stripe_checkout/visit_views.py
index 05a908699f4cb121908e87c470b76d3aa099ac7f..85d7c3960dc9cb25e361b4928bd3de5ac38fb12f 100644
--- a/stripe_checkout/stripe_checkout/visit_views.py
+++ b/stripe_checkout/stripe_checkout/visit_views.py
@@ -6,7 +6,7 @@ from django import forms
 from django.http import Http404, HttpResponse
 from django.shortcuts import redirect, render
 from django.views.decorators.csrf import csrf_exempt
-from django.views.decorators.http import require_GET, require_http_methods, require_POST
+from django.views.decorators.http import require_http_methods, require_POST
 
 from . import stripe_ as stripe
 from .models import Event, ExchangeRate
@@ -31,9 +31,7 @@ def checkout(request, visitor_id):
     status_code = 200
     if has_outstanding_order(visitor_id):
         if request.POST:
-            response = redirect(
-                "stripe_checkout:checkout-entrypoint", visitor_id=visitor_id
-            )
+            response = redirect("stripe_checkout:checkout", visitor_id=visitor_id)
             return response
         else:
             return render(
@@ -90,21 +88,6 @@ def create_invoice(visitor, data, shopping_cart, order):
     )
 
 
-@require_GET
-def checkout_success(request, visitor_id):
-    # Here we just show a success message. Updating visit will be handled
-    # asynchronously by the stripe event webhook handler. This endpoint is not
-    # sufficiently secure to update payment status
-    return render(
-        request,
-        "invoice-created.html",
-        context={
-            "visitor_id": visitor_id,
-            "body_classes": "tnc-background",
-        },
-    )
-
-
 @csrf_exempt
 @require_POST
 @whitelist_ips(by_setting="STRIPE_WEBHOOK_ALLOWED_IPS")
diff --git a/test/test_visit.py b/test/test_visit.py
index 56635ce47abb0f995494cd416112414de478f4c0..6ed9f552262f696919e39b5a0b9f51dcf69cb56e 100644
--- a/test/test_visit.py
+++ b/test/test_visit.py
@@ -3,13 +3,14 @@ from unittest.mock import patch
 import pytest
 import responses
 import stripe
-
+from django.contrib.auth.models import Permission
 from stripe_checkout.stripe_checkout.models import Event, Order
 
 
 @responses.activate
 @pytest.mark.django_db
 def test_list_visitors(client, user):
+    user.user_permissions.add(Permission.objects.get(codename="manage_visitors"))
     client.force_login(user)
     rv = client.get("/visitors/")
     assert rv.status_code == 200