PayTheory Payment Processor
This Canvas EMR plugin integrates Pay Theory as a custom credit card payment processor, letting Canvas collect payments and manage a patient’s saved cards through a third-party provider instead of the built-in Stripe integration.
SDK Features #
- Extends the
CardPaymentProcessorhandler - Responds to the
REVENUE__PAYMENT_PROCESSOR__*events - Renders tokenization forms with the
PaymentProcessorFormeffect and Django templates - Charges cards and returns a
CardTransactioneffect - Lists, adds, and removes saved cards with the
PaymentMethod,AddPaymentMethodResponse, andRemovePaymentMethodResponseeffects - Reads configuration from plugin variables and declares
url_permissionsfor the Pay Theory JS SDK
CANVAS_MANIFEST.json #
{
"sdk_version": "0.1.4",
"plugin_version": "0.0.1",
"name": "paytheory_payment_processor",
"description": "PayTheory credit card payment processor integration",
"url_permissions": [
{
"url": "https://*.sdk.paytheory.com/index.js",
"permissions": ["SCRIPTS"]
}
],
"components": {
"handlers": [
{
"class": "paytheory_payment_processor.handlers.paytheory_payment_processor:PayTheoryPaymentProcessor",
"description": "Handles credit card payments, tokenization, and payment method management via PayTheory",
"data_access": {
"event": "",
"read": [],
"write": []
}
}
],
"commands": [],
"content": [],
"effects": [],
"views": []
},
"variables": [
{"name": "paytheory_merchant_id", "sensitive": true},
{"name": "paytheory_public_key"},
{"name": "paytheory_secret_key", "sensitive": true},
{"name": "paytheory_partner"},
{"name": "paytheory_environment"}
],
"tags": {},
"references": [],
"license": "",
"readme": "./README.md"
}
The url_permissions above are abbreviated — the full manifest lists every Pay Theory SDK, token-service, and tags-static URL across the production, sandbox, and lab environments.
handlers/ #
paytheory_payment_processor.py #
Purpose and Functionality
This file defines PayTheoryPaymentProcessor, a subclass of CardPaymentProcessor. It implements the methods Canvas calls throughout a payment workflow: rendering the tokenization forms, charging a card, and listing/adding/removing a patient’s saved cards. Card data is tokenized client-side by Pay Theory’s JS SDK, so the plugin never handles raw card numbers.
Core Logic
payment_form/add_card_formrender the Pay Theory JS SDK form (via a Django template) for paying or saving a card.chargesends the tokenized payment method to Pay Theory’s GraphQL API and maps the result onto aCardTransaction, treatingPENDING/SETTLED/SUCCESS/SUCCEEDEDstatuses as successful.payment_methodsmaps a patient to a Pay Theory payor (viacanvas_patient_idmetadata) and returns their saved cards asPaymentMethodeffects.add_payment_method/remove_payment_methodsave and disable cards for the patient’s payor.
from decimal import Decimal
from typing import Any
from canvas_sdk.effects.payment_processor import (
AddPaymentMethodResponse,
CardTransaction,
PaymentMethod,
PaymentProcessorForm,
RemovePaymentMethodResponse,
)
from canvas_sdk.handlers.payment_processors.card import CardPaymentProcessor
from canvas_sdk.templates import render_to_string
from canvas_sdk.v1.data import Patient
class PayTheoryPaymentProcessor(CardPaymentProcessor):
"""Custom payment processor for handling credit card payments with PayTheory."""
def payment_form(self, patient: Patient | None = None) -> PaymentProcessorForm:
content = render_to_string(
"templates/form.html",
{
"intent": self.PaymentIntent.PAY,
"public_api_key": self.public_api_key,
"sdk_url": self.sdk_url,
},
)
return PaymentProcessorForm(content=content, intent=self.PaymentIntent.PAY)
def charge(
self, amount: Decimal, token: str, patient: Patient | None = None, **kwargs: Any
) -> CardTransaction:
transaction = self.api.create_transaction(
TransactionInput(payment_method_id=token, amount=amount)
)
status = (transaction.get("status") or "").upper()
return CardTransaction(
success=status in SUCCESS_STATUSES,
transaction_id=transaction["transaction_id"],
api_response=transaction,
)
def payment_methods(self, patient: Patient | None = None) -> list[PaymentMethod]:
if not patient:
return []
payor_id = self.get_or_create_payor_id(patient)
if not payor_id:
return []
return [self._to_payment_method(m) for m in self.api.get_payment_methods(payor_id=payor_id)]
def add_payment_method(
self, token: str, patient: Patient, **kwargs: Any
) -> AddPaymentMethodResponse:
return AddPaymentMethodResponse(success=True)
def remove_payment_method(self, token: str, patient: Patient) -> RemovePaymentMethodResponse:
result = self.api.disable_payment_method(payment_method_id=token)
return RemovePaymentMethodResponse(success=result)
The excerpt above is abridged; see the source on GitHub for the full implementation, the Pay Theory API client, and the form template.