Writing a payment provider plugin

In this document, we will walk through the creation of a payment provider plugin. This is very similar to creating an export output.

Please read Creating a plugin first, if you haven’t already.

Warning

We changed our payment provider API a lot in pretix 2.x. Our documentation page on Porting a payment provider from pretix 1.x to pretix 2.x might be insightful even if you do not have a payment provider to port, as it outlines the rationale behind the current design.

Provider registration

The payment provider API does not make a lot of usage from signals, however, it does use a signal to get a list of all available payment providers. Your plugin should listen for this signal and return the subclass of pretix.base.payment.BasePaymentProvider that the plugin will provide:

1from django.dispatch import receiver
2
3from pretix.base.signals import register_payment_providers
4
5
6@receiver(register_payment_providers, dispatch_uid="payment_paypal")
7def register_payment_provider(sender, **kwargs):
8    from .payment import Paypal
9    return Paypal

The provider class

class pretix.base.payment.BasePaymentProvider

The central object of each payment provider is the subclass of BasePaymentProvider.

BasePaymentProvider.event

The default constructor sets this property to the event we are currently working for.

BasePaymentProvider.settings

The default constructor sets this property to a SettingsSandbox object. You can use this object to store settings using its get and set methods. All settings you store are transparently prefixed, so you get your very own settings namespace.

BasePaymentProvider.identifier

A short and unique identifier for this payment provider. This should only contain lowercase letters and in most cases will be the same as your package name.

This is an abstract attribute, you must override this!

BasePaymentProvider.verbose_name

A human-readable name for this payment provider. This should be short but self-explaining. Good examples include ‘Bank transfer’ and ‘Credit card via Stripe’.

This is an abstract attribute, you must override this!

BasePaymentProvider.public_name

A human-readable name for this payment provider to be shown to the public. This should be short but self-explaining. Good examples include ‘Bank transfer’ and ‘Credit card’, but ‘Credit card via Stripe’ might be to explicit. By default, this is the same as verbose_name

BasePaymentProvider.confirm_button_name

A label for the “confirm” button on the last page before a payment is started. This is not used in the regular checkout flow, but only if the payment method is selected for an existing order later on.

BasePaymentProvider.is_enabled

Returns whether or whether not this payment provider is enabled. By default, this is determined by the value of the _enabled setting.

BasePaymentProvider.priority

Returns a priority that is used for sorting payment providers. Higher priority means higher up in the list. Default to 100. Providers with same priority are sorted alphabetically.

BasePaymentProvider.settings_form_fields

When the event’s administrator visits the event configuration page, this method is called to return the configuration fields available.

It should therefore return a dictionary where the keys should be (unprefixed) settings keys and the values should be corresponding Django form fields.

The default implementation returns the appropriate fields for the _enabled, _fee_abs, _fee_percent and _availability_date settings mentioned above.

We suggest that you return an OrderedDict object instead of a dictionary and make use of the default implementation. Your implementation could look like this:

 1@property
 2def settings_form_fields(self):
 3    return OrderedDict(
 4        list(super().settings_form_fields.items()) + [
 5            ('bank_details',
 6             forms.CharField(
 7                 widget=forms.Textarea,
 8                 label=_('Bank account details'),
 9                 required=False
10             ))
11        ]
12    )

Warning

It is highly discouraged to alter the _enabled field of the default implementation.

BasePaymentProvider.walletqueries

Warning

This property is considered experimental. It might change or get removed at any time without prior notice.

A list of wallet payment methods that should be dynamically joined to the public name of the payment method, if they are available to the user. The detection is made on a best effort basis with no guarantees of correctness and actual availability. Wallets that pretix can check for are exposed through pretix.base.payment.WalletQueries.

BasePaymentProvider.settings_form_clean(cleaned_data)

Overriding this method allows you to inject custom validation into the settings form.

Parameters:

cleaned_data – Form data as per previous validations.

Returns:

Please return the modified cleaned_data

BasePaymentProvider.settings_content_render(request: HttpRequest) str

When the event’s administrator visits the event configuration page, this method is called. It may return HTML containing additional information that is displayed below the form fields configured in settings_form_fields.

BasePaymentProvider.is_allowed(request: HttpRequest, total: Decimal | None = None) bool

You can use this method to disable this payment provider for certain groups of users, products or other criteria. If this method returns False, the user will not be able to select this payment method. This will only be called during checkout, not on retrying.

The default implementation checks for the _availability_date setting to be either unset or in the future and for the _availability_from, _total_max, and _total_min requirements to be met. It also checks the _restrict_countries and _restrict_to_sales_channels setting.

Parameters:

total – The total value without the payment method fee, after taxes.

Changed in version 1.17.0: The total parameter has been added. For backwards compatibility, this method is called again without this parameter if it raises a TypeError on first try.

BasePaymentProvider.payment_form_render(request: HttpRequest, total: Decimal, order: Order | None = None) str

When the user selects this provider as their preferred payment method, they will be shown the HTML you return from this method.

The default implementation will call payment_form() and render the returned form. If your payment method doesn’t require the user to fill out form fields, you should just return a paragraph of explanatory text.

Parameters:

order – Only set when this is a change to a new payment method for an existing order.

BasePaymentProvider.payment_form(request: HttpRequest) Form

This is called by the default implementation of payment_form_render() to obtain the form that is displayed to the user during the checkout process. The default implementation constructs the form using payment_form_fields and sets appropriate prefixes for the form and all fields and fills the form with data form the user’s session.

If you overwrite this, we strongly suggest that you inherit from PaymentProviderForm (from this module) that handles some nasty issues about required fields for you.

BasePaymentProvider.payment_form_fields

This is used by the default implementation of payment_form(). It should return an object similar to settings_form_fields.

The default implementation returns an empty dictionary.

BasePaymentProvider.payment_is_valid_session(request: HttpRequest) bool

This is called at the time the user tries to place the order. It should return True if the user’s session is valid and all data your payment provider requires in future steps is present.

BasePaymentProvider.checkout_prepare(request: HttpRequest, cart: Dict[str, Any]) bool | str

Will be called after the user selects this provider as their payment method. If you provided a form to the user to enter payment data, this method should at least store the user’s input into their session.

This method should return False if the user’s input was invalid, True if the input was valid and the frontend should continue with default behavior or a string containing a URL if the user should be redirected somewhere else.

On errors, you should use Django’s message framework to display an error message to the user (or the normal form validation error messages).

The default implementation stores the input into the form returned by payment_form() in the user’s session.

If your payment method requires you to redirect the user to an external provider, this might be the place to do so.

Important

If this is called, the user has not yet confirmed their order. You may NOT do anything which actually moves money.

Note: The behavior of this method changes significantly when you set

multi_use_supported. Please refer to the multi_use_supported documentation for more information.

Parameters:

cart

This dictionary contains at least the following keys:

positions:

A list of CartPosition objects that are annotated with the special attributes count and total because multiple objects of the same content are grouped into one.

raw:

The raw list of CartPosition objects in the users cart

total:

The overall total including the fee for the payment method.

payment_fee:

The fee for the payment method.

BasePaymentProvider.checkout_confirm_render(request, order: Order | None = None, info_data: dict | None = None) str

If the user has successfully filled in their payment data, they will be redirected to a confirmation page which lists all details of their order for a final review. This method should return the HTML which should be displayed inside the ‘Payment’ box on this page.

In most cases, this should include a short summary of the user’s input and a short explanation on how the payment process will continue.

Parameters:
  • request – The current HTTP request.

  • order – Only set when this is a change to a new payment method for an existing order.

  • info_data – The info_data dictionary you set during add_payment_to_cart (only filled if multi_use_supported is set)

This is an abstract method, you must override this!

BasePaymentProvider.execute_payment(request: HttpRequest, payment: OrderPayment) str

After the user has confirmed their purchase, this method will be called to complete the payment process. This is the place to actually move the money if applicable. You will be passed an pretix.base.models.OrderPayment object that contains the amount of money that should be paid.

If you need any special behavior, you can return a string containing the URL the user will be redirected to. If you are done with your process you should return the user to the order’s detail page. Redirection is only allowed if you set execute_payment_needs_user to True.

If the payment is completed, you should call payment.confirm(). Please note that this might raise a Quota.QuotaExceededException if (and only if) the payment term of this order is over and some of the items are sold out. You should use the exception message to display a meaningful error to the user.

The default implementation just returns None and therefore leaves the order unpaid. The user will be redirected to the order’s detail page by default.

On errors, you should raise a PaymentException.

Parameters:
  • request – A HTTP request, except if execute_payment_needs_user is False

  • payment – An OrderPayment instance

BasePaymentProvider.calculate_fee(price: Decimal) Decimal

Calculate the fee for this payment provider which will be added to final price before fees (but after taxes). It should include any taxes. The default implementation makes use of the setting _fee_abs for an absolute fee and _fee_percent for a percentage.

Parameters:

price – The total value without the payment method fee, after taxes.

BasePaymentProvider.order_pending_mail_render(order: Order, payment: OrderPayment) str

After the user has submitted their order, they will receive a confirmation email. You can return a string from this method if you want to add additional information to this email.

Parameters:
  • order – The order object

  • payment – The payment object

BasePaymentProvider.payment_pending_render(request: HttpRequest, payment: OrderPayment) str

Render customer-facing instructions on how to proceed with a pending payment

Returns:

HTML

BasePaymentProvider.abort_pending_allowed

Whether or not a user can abort a payment in pending state to switch to another payment method. This returns False by default which is no guarantee that aborting a pending payment can never happen, it just hides the frontend button to avoid users accidentally committing double payments.

BasePaymentProvider.render_invoice_text(order: Order, payment: OrderPayment) str

This is called when an invoice for an order with this payment provider is generated. The default implementation returns the content of the _invoice_text configuration variable (an I18nString), or an empty string if unconfigured. For paid orders, the default implementation always renders a string stating that the invoice is already paid.

BasePaymentProvider.render_invoice_stamp(order: Order, payment: OrderPayment) str

This is called when an invoice for an order with this payment provider is generated. The default implementation returns “paid” if the order was already paid, and None otherwise. You can override this with a string, but it should be really short to make the invoice look pretty.

BasePaymentProvider.order_change_allowed(order: Order, request: HttpRequest | None = None) bool

Will be called to check whether it is allowed to change the payment method of an order to this one.

The default implementation checks for the _availability_date setting to be either unset or in the future, as well as for the _availability_from, _total_max, _total_min, and _restricted_countries settings.

Parameters:

order – The order object

BasePaymentProvider.payment_prepare(request: HttpRequest, payment: OrderPayment) bool | str

Will be called if the user retries to pay an unpaid order (after the user filled in e.g. the form returned by payment_form()) or if the user changes the payment method.

It should return and report errors the same way as checkout_prepare(), but receives an Order object instead of a cart object.

Note: The Order object given to this method might be different from the version stored in the database as it’s total will already contain the payment fee for the new payment method.

BasePaymentProvider.payment_control_render(request: HttpRequest, payment: OrderPayment) str

Will be called if the event administrator views the details of a payment.

It should return HTML code containing information regarding the current payment status and, if applicable, next steps.

The default implementation returns an empty string.

Parameters:

order – The order object

BasePaymentProvider.payment_control_render_short(payment: OrderPayment) str

Will be called if the event administrator performs an action on the payment. Should return a very short version of the payment method. Usually, this should return e.g. an account identifier of the payee, but no information on status, dates, etc.

The default implementation falls back to payment_presale_render.

Parameters:

payment – The payment object

BasePaymentProvider.payment_refund_supported(payment: OrderPayment) bool

Will be called to check if the provider supports automatic refunding for this payment.

BasePaymentProvider.payment_partial_refund_supported(payment: OrderPayment) bool

Will be called to check if the provider supports automatic partial refunding for this payment.

BasePaymentProvider.payment_presale_render(payment: OrderPayment) str

Will be called if the ticket customer views the details of a payment. This is currently used e.g. when the customer requests a refund to show which payment method is used for the refund. This should only include very basic information about the payment, such as “VISA card …9999”, and never raw payment information.

The default implementation returns the public name of the payment provider.

Parameters:

order – The order object

BasePaymentProvider.execute_refund(refund: OrderRefund)

Will be called to execute an refund. Note that refunds have an amount property and can be partial.

This should transfer the money back (if possible). On success, you should call refund.done(). On failure, you should raise a PaymentException.

BasePaymentProvider.refund_control_render(request: HttpRequest, refund: OrderRefund) str

Will be called if the event administrator views the details of a refund.

It should return HTML code containing information regarding the current refund status and, if applicable, next steps.

The default implementation returns an empty string.

Parameters:

refund – The refund object

BasePaymentProvider.refund_control_render_short(refund: OrderRefund) str

Will be called if the event administrator performs an action on the refund. Should return a very short description of the refund method. Usually, this should return e.g. an account identifier of the refund recipient, but no information on status, dates, etc.

The default implementation returns an empty string.

Parameters:

refund – The refund object

BasePaymentProvider.new_refund_control_form_render(request: HttpRequest, order: Order) str

Render a form that will be shown to backend users when trying to create a new refund.

Usually, refunds are created from an existing payment object, e.g. if there is a credit card payment and the credit card provider returns True from payment_refund_supported, the system will automatically create an OrderRefund and call execute_refund on that payment. This method can and should not be used in that situation! Instead, by implementing this method you can add a refund flow for this payment provider that starts without an existing payment. For example, even though an order was paid by credit card, it could easily be refunded by SEPA bank transfer. In that case, the SEPA bank transfer provider would implement this method and return a form that asks for the IBAN.

This method should return HTML or None. All form fields should have a globally unique name.

BasePaymentProvider.new_refund_control_form_process(request: HttpRequest, amount: Decimal, order: Order) OrderRefund

Process a backend user’s request to initiate a new refund with an amount of amount for order.

This method should parse the input provided to the form created and either raise ValidationError or return an OrderRefund object in created state that has not yet been saved to the database. The system will then call execute_refund on that object.

BasePaymentProvider.api_payment_details(payment: OrderPayment)

Will be called to populate the details parameter of the payment in the REST API.

Parameters:

payment – The payment in question.

Returns:

A serializable dictionary

BasePaymentProvider.api_refund_details(refund: OrderRefund)

Will be called to populate the details parameter of the refund in the REST API.

Parameters:

refund – The refund in question.

Returns:

A serializable dictionary

BasePaymentProvider.matching_id(payment: OrderPayment)

Will be called to get an ID for matching this payment when comparing pretix records with records of an external source. This should return the main transaction ID for your API.

Parameters:

payment – The payment in question.

Returns:

A string or None

BasePaymentProvider.refund_matching_id(refund: OrderRefund)

Will be called to get an ID for matching this refund when comparing pretix records with records of an external source. This should return the main transaction ID for your API.

Parameters:

refund – The refund in question.

Returns:

A string or None

BasePaymentProvider.shred_payment_info(obj: OrderPayment | OrderRefund)

When personal data is removed from an event, this method is called to scrub payment-related data from a payment or refund. By default, it removes all info from the info attribute. You can override this behavior if you want to retain attributes that are not personal data on their own, i.e. a reference to a transaction in an external system. You can also override this to scrub more data, e.g. data from external sources that is saved in LogEntry objects or other places.

Parameters:

order – An order

BasePaymentProvider.cancel_payment(payment: OrderPayment)

Will be called to cancel a payment. The default implementation just sets the payment state to canceled, but in some cases you might want to notify an external provider.

On success, you should set payment.state = OrderPayment.PAYMENT_STATE_CANCELED (or call the super method). On failure, you should raise a PaymentException.

BasePaymentProvider.is_implicit = <function BasePaymentProvider.is_implicit>
BasePaymentProvider.is_meta

Returns whether or whether not this payment provider is a “meta” payment provider that only works as a settings holder for other payment providers and should never be used directly. This is a trick to implement payment gateways with multiple payment methods but unified payment settings. Take a look at the built-in stripe provider to see how this might be used. By default, this returns False.

BasePaymentProvider.execute_payment_needs_user

Set this to True if your execute_payment function needs to be triggered by a user request, i.e. either needs the request object or might require a browser redirect. If this is False, you will not receive a request and may not redirect since execute_payment might be called server-side. You should ensure that your execute_payment method has a limited execution time (i.e. by using timeout for all external calls) and handles all error cases appropriately.

BasePaymentProvider.multi_use_supported

Returns whether or whether not this payment provider supports being used multiple times in the same checkout, or in addition to a different payment provider. This is usually only useful for payment providers that represent gift cards, i.e. payment methods with an upper limit per payment instrument that can usually be combined with other instruments.

If you set this property to True, the behavior of how pretix interacts with your payment provider changes and you will need to respect the following rules:

  • payment_form_render must not depend on session state, it must always allow a user to add a new payment.

    Editing a payment is not possible, but pretix will give users an option to delete it.

  • Returning True from checkout_prepare is no longer enough. Instead, you must also call pretix.base.services.cart.add_payment_to_cart(request, provider, min_value, max_value, info_data) to add the payment to the session. You are still allowed to do a redirect from checkout_prepare and then call this function upon return.

  • Unlike in the general case, when checkout_prepare is called, the cart['total'] parameter will not yet include payment fees charged by your provider as we don’t yet know the amount of the charge, so you need to take care of that yourself when setting your maximum amount.

  • payment_is_valid_session will not be called during checkout, don’t rely on it. If you called add_payment_to_cart, we’ll trust the payment is okay and your next chance to change that will be execute_payment.

The changed behavior currently only affects the behavior during initial checkout (i.e. checkout_prepare), for payment_prepare the regular behavior applies and you are expected to just modify the amount of the OrderPayment object if you need to.

BasePaymentProvider.test_mode_message

If this property is set to a string, this will be displayed when this payment provider is selected while the event is in test mode. You should use it to explain to your user how your plugin behaves, e.g. if it falls back to a test mode automatically as well or if actual payments will be performed.

If you do not set this (or, return None), pretix will show a default message warning the user that this plugin does not support test mode payments.

BasePaymentProvider.requires_invoice_immediately

Return whether this payment method requires an invoice to exist for an order, even though the event is configured to only create invoices for paid orders. By default this is False, but it might be overwritten for e.g. bank transfer. execute_payment is called after the invoice is created.

Additional views

See also: Creating custom views.

For most simple payment providers it is more than sufficient to implement some of the BasePaymentProvider methods. However, in some cases it is necessary to introduce additional views. One example is the PayPal provider. It redirects the user to a PayPal website in the BasePaymentProvider.checkout_prepare() step of the checkout process and provides PayPal with a URL to redirect back to. This URL points to a view which looks roughly like this:

 1@login_required
 2def success(request):
 3    pid = request.GET.get('paymentId')
 4    payer = request.GET.get('PayerID')
 5    # We stored some information in the session in checkout_prepare(),
 6    # let's compare the new information to double-check that this is about
 7    # the same payment
 8    if pid == request.session['payment_paypal_id']:
 9        # Save the new information to the user's session
10        request.session['payment_paypal_payer'] = payer
11        try:
12            # Redirect back to the confirm page. We chose to save the
13            # event ID in the user's session. We could also put this
14            # information into a URL parameter.
15            event = Event.objects.current.get(identity=request.session['payment_paypal_event'])
16            return redirect(reverse('presale:event.checkout.confirm', kwargs={
17                'event': event.slug,
18                'organizer': event.organizer.slug,
19            }))
20        except Event.DoesNotExist:
21            pass  # TODO: Display error message
22    else:
23        pass  # TODO: Display error message

If you do not want to provide a view of your own, you could even let PayPal redirect directly back to the confirm page and handle the query parameters inside BasePaymentProvider.checkout_is_valid_session(). However, because some external providers (not PayPal) force you to have a constant redirect URL, it might be necessary to define custom views.