To integrate 2Checkout with a Django app, send the buyer to 2Checkout's hosted (ConvertPlus/Inline) checkout, then confirm the payment server-side when 2Checkout calls your webhook - the Instant Notification System (INS) - validating its MD5 signature and updating an Order model you control. One honest caveat first: 2Checkout was acquired by Verifone in 2020 and is now branded 2Checkout (now Verifone), part of Verifone's commerce platform. The legacy 2Checkout building blocks (the hosted checkout, ConvertPlus/Inline, and the INS webhook) still work under Verifone, so an integration written against them remains valid - the dashboards and branding are simply Verifone's now.
Key takeaways
- 2Checkout is now Verifone (acquired 2020); the classic checkout and INS webhook still operate under the Verifone brand.
- Let the hosted/inline checkout collect the card - your Django app never touches raw card numbers, which keeps PCI scope low.
- Verify every payment server-side via the INS webhook: recompute the MD5 hash from your secret word and compare it in constant time before marking an order paid.
- Never trust amounts from the browser. Look up the order you stored and trust your own figure, not the value posted back.
- Keep secrets in environment variables, force HTTPS, and exempt only the webhook from CSRF (it comes from Verifone, not your form).
- Stripe is the common modern default for Django; 2Checkout/Verifone earns its place when you need a global merchant of record that handles cross-border tax and many currencies.
Is 2Checkout still available after the Verifone acquisition?
Yes. Verifone acquired 2Checkout in 2020 and folded it into its commerce platform; you will see it referred to as 2Checkout (now Verifone). When you log into the merchant area you are using Verifone's dashboard, but the building blocks this guide relies on - the hosted checkout, the ConvertPlus/Inline overlay, and the INS (Instant Notification System) webhook - are the same long-standing 2Checkout features. Existing integrations keep working; only the branding and some dashboard URLs changed. Treat any old tutorial's flow as still broadly correct, but update the Python (this 2016 post originally targeted Python 2) and read amounts and secrets from configuration instead of hard-coding them.
Which 2Checkout/Verifone checkout flow should you use?
2Checkout exposes a few ways to take a payment. For almost every Django app the hosted options are the right call, because the card is entered on Verifone's secure form rather than yours - that keeps you out of the riskiest part of PCI compliance.
| Flow | What it is | Where the card is entered | Use it when |
|---|---|---|---|
| ConvertPlus / Inline | Hosted payment form shown as an overlay on your page | Verifone's hosted iframe | You want a modern overlay without a full redirect |
| Standard hosted checkout | Full redirect to Verifone's checkout page | Verifone's page | You want the simplest flow and lowest PCI scope |
| Payment API (legacy) | You collect the card and tokenize with 2co.js |
Your page (then tokenized) | You need full control - now restricted, needs approval |
The Payment API (collect the card yourself, tokenize with 2co.js, then authorize server-side) gives the most control but now needs account approval and puts you in a higher PCI bracket, so most teams skip it. The rest of this guide uses the hosted/inline flow plus the INS webhook.
How do you configure 2Checkout (Verifone) credentials in Django?
Never hard-code your seller ID or secret word. Read them from environment variables so they stay out of source control and can differ per environment. You set the secret word yourself in the merchant area; it is the shared secret used to sign and verify INS messages.
# settings.py - read every credential from the environment, never hard-code it
import os
TWOCHECKOUT_SELLER_ID = os.environ["TWOCHECKOUT_SELLER_ID"] # your account / vendor number
TWOCHECKOUT_SECRET_WORD = os.environ["TWOCHECKOUT_SECRET_WORD"] # set this in the merchant area
TWOCHECKOUT_SANDBOX = os.environ.get("TWOCHECKOUT_SANDBOX", "1") == "1"
# Hosted-checkout endpoint (the form below adds demo=Y while in sandbox)
TWOCHECKOUT_PURCHASE_URL = "https://secure.2checkout.com/checkout/purchase"How do you send a buyer to the hosted checkout?
Store the order in your database before redirecting, then post a small form to the hosted checkout. Pass your own merchant_order_id so the webhook can find the order later, and set x_receipt_link_url to the page buyers return to. Loading 2Checkout's inline script turns the standard hosted checkout into the inline (ConvertPlus) overlay.
<!-- order_summary.html: post the cart to Verifone's hosted/inline checkout -->
<!-- Loading the inline script opens the hosted form as an overlay (ConvertPlus) -->
<script src="https://www.2checkout.com/static/checkout/javascript/direct.min.js"></script>
<form action="{{ purchase_url }}" method="post">
<input type="hidden" name="sid" value="{{ seller_id }}">
<input type="hidden" name="mode" value="2CO">
<input type="hidden" name="merchant_order_id" value="{{ order.id }}">
<input type="hidden" name="li_0_type" value="product">
<input type="hidden" name="li_0_name" value="Pro plan">
<input type="hidden" name="li_0_price" value="{{ order.amount }}">
<input type="hidden" name="currency_code" value="{{ order.currency }}">
<input type="hidden" name="x_receipt_link_url" value="https://yoursite.com/payments/return/">
{% if sandbox %}<input type="hidden" name="demo" value="Y">{% endif %}
<button type="submit">Pay securely</button>
</form>How do you model and verify the order in Django?
Keep an Order model as the single source of truth. Create it as pending, then let the webhook flip it to paid. Tie each order to the buyer with a foreign key to AUTH_USER_MODEL so it works whether you use Django's default user or a custom user model in Django.
# models.py
from django.conf import settings
from django.db import models
class Order(models.Model):
class Status(models.TextChoices):
PENDING = "pending", "Pending"
PAID = "paid", "Paid"
FAILED = "failed", "Failed"
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="orders"
)
amount = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=3, default="USD")
status = models.CharField(
max_length=10, choices=Status.choices, default=Status.PENDING
)
# Verifone/2Checkout references, filled in by the webhook
sale_id = models.CharField(max_length=64, blank=True)
invoice_id = models.CharField(max_length=64, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Order #{self.pk} ({self.status})"How do you confirm payment with the INS webhook?
The Instant Notification System (INS) is 2Checkout/Verifone's server-to-server callback, and it is the only trustworthy confirmation - the buyer's return to x_receipt_link_url is for showing a thank-you page, never for granting access. Each INS message carries an md5_hash; recompute it from sale_id + vendor_id + invoice_id + secret_word, upper-case it, and compare in constant time. Then look up the order you stored and trust your own amount, not anything in the request.
# views.py
import hashlib
import hmac
from django.conf import settings
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from .models import Order
def _ins_signature_ok(post):
"""Recompute 2Checkout's INS MD5 hash and compare it in constant time."""
raw = "{sale_id}{vendor_id}{invoice_id}{secret}".format(
sale_id=post.get("sale_id", ""),
vendor_id=post.get("vendor_id", ""),
invoice_id=post.get("invoice_id", ""),
secret=settings.TWOCHECKOUT_SECRET_WORD,
)
expected = hashlib.md5(raw.encode()).hexdigest().upper()
return hmac.compare_digest(expected, post.get("md5_hash", "").upper())
@csrf_exempt # the call comes from Verifone's servers, not your own form
@require_POST
def twocheckout_ins(request):
post = request.POST
if not _ins_signature_ok(post):
return HttpResponseForbidden("invalid signature")
# Trust the order you stored - never the amount posted by the client
order = get_object_or_404(Order, pk=post.get("merchant_order_id"))
# Idempotent: if it is already paid, acknowledge and do nothing
if order.status == Order.Status.PAID:
return HttpResponse("ok")
if (
post.get("message_type") == "ORDER_CREATED"
and post.get("invoice_status") == "approved"
):
order.status = Order.Status.PAID
order.sale_id = post.get("sale_id", "")
order.invoice_id = post.get("invoice_id", "")
order.save(update_fields=["status", "sale_id", "invoice_id"])
# Reply 200 quickly so Verifone stops retrying the notification
return HttpResponse("ok")Wire the webhook to a URL and register that URL as your INS endpoint in the merchant area:
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path("payments/ins/", views.twocheckout_ins, name="twocheckout_ins"),
]How do you keep the integration secure?
Payment code is where small mistakes become expensive, so treat these as non-negotiable:
- Verify server-side, always. A payment is real only after the INS signature checks out and you have matched the order in your database.
- Never trust client-side amounts. The browser can change any hidden field, so charge and record the amount from your own
Order, not from the POST. - Validate the signature in constant time with
hmac.compare_digestto avoid timing attacks - a plain==leaks information. (The legacy INS hash is MD5, so treat it as a shared-secret check and always combine it with the database lookup above.) - Keep secrets out of code. The seller ID and secret word live in environment variables, never in the repo or templates.
- Force HTTPS on the return and webhook URLs.
- Make the webhook idempotent - if the order is already
paid, return 200 and do nothing so a retried INS message cannot double-apply.
2Checkout/Verifone vs Stripe vs PayPal for Django
Be pragmatic about the gateway. Stripe is the most common modern default for Django products and SaaS thanks to its API quality and the official stripe SDK. PayPal is quick to add and carries strong consumer trust - see our walkthrough of PayPal integration with Django. 2Checkout (now Verifone) stands out when you sell globally and want a merchant of record that handles cross-border tax/VAT and many currencies for you.
| Factor | 2Checkout (now Verifone) | Stripe | PayPal |
|---|---|---|---|
| Best fit | Global, multi-currency selling | Modern default for SaaS & products | Quick add, high buyer trust |
| Merchant of record | Yes (handles cross-border tax/VAT) | No (you are the merchant) | No (you are the merchant) |
| Hosted checkout | ConvertPlus / Inline overlay | Stripe Checkout / Payment Links | PayPal Checkout buttons |
| Direct API | Legacy Payment API (restricted) | Rich, well-documented API | REST API |
| Recurring billing | Built-in subscriptions | Billing / Subscriptions | Subscriptions |
| Django tooling | Thin - mostly DIY | Official stripe SDK, large community |
paypal-server-sdk / community |
| Currency reach | Very broad (100+ currencies) | Broad | Broad |
| Transaction fee | Per-transaction - see provider | Per-transaction - see provider | Per-transaction - see provider |
The fees are deliberately left out here - each provider charges a per-transaction fee that changes over time, so check the provider's current pricing page. If you would rather hand the whole integration off, our Django development services team wires up gateways, webhooks, and reconciliation for production.
Frequently Asked Questions
Is 2Checkout still available?
Yes. Verifone acquired 2Checkout in 2020 and it now operates as 2Checkout (now Verifone) inside Verifone's commerce platform. The hosted checkout, the ConvertPlus/Inline overlay, and the INS webhook all still work, so existing integrations keep running - mainly the dashboard branding and some URLs changed.
How do I verify a 2Checkout payment in Django?
Do not rely on the buyer returning to your site. Use the INS webhook: recompute the MD5 hash from sale_id + vendor_id + invoice_id + secret_word, upper-case it, and compare it to the posted md5_hash with hmac.compare_digest. Only then look up the order you stored and mark it paid using your own amount.
Should I use 2Checkout/Verifone or Stripe for a Django app?
Stripe is the common modern default for Django because of its API quality and official SDK. Choose 2Checkout/Verifone when you want a merchant of record that handles cross-border tax and many currencies, which simplifies selling globally. PayPal is a fast add when buyer trust at checkout matters most.
Is the legacy 2Checkout Payment API still usable?
The Payment API (collect the card, tokenize with 2co.js, authorize server-side) still exists under Verifone but typically needs account approval and puts you in a higher PCI bracket. Most Django teams use the hosted/inline checkout instead, so card data never touches their servers.
How do I keep my 2Checkout secret word safe in Django?
Store the seller ID and secret word in environment variables and read them via os.environ in settings.py - never commit them or put them in templates. The secret word is the shared key that signs INS messages, so rotate it in the merchant area if it is ever exposed.
Do I need to disable CSRF for the 2Checkout webhook?
Yes - the INS request comes from Verifone's servers, not your form, so it has no CSRF token. Decorate the webhook view with @csrf_exempt, but compensate by validating the MD5 signature and matching the order in your database before trusting the message.