Claim Effects

The Canvas SDK provides effects to facilitate managing claims. The ClaimEffect class provides a unified interface for:

Additionally, the SDK provides a separate effect to update claim line items.

The following standalone effect classes are deprecated and will be removed in a future release. Please use the ClaimEffect class instead.

Deprecated ClassOld Import PathNew Equivalent
AddClaimLabelcanvas_sdk.effects.claim_labelClaimEffect.add_labels()
RemoveClaimLabelcanvas_sdk.effects.claim_labelClaimEffect.remove_labels()
MoveClaimToQueuecanvas_sdk.effects.claim_queueClaimEffect.move_to_queue()
AddClaimCommentcanvas_sdk.effects.claim_commentClaimEffect.add_comment()
PostClaimPaymentcanvas_sdk.effects.paymentClaimEffect.post_payment()

Claim Effect #

The ClaimEffect class facilitates operations on existing claims.

from canvas_sdk.effects.claim import ClaimEffect

Attributes #

AttributeTypeDescriptionRequired
claim_idUUID or strIdentifier for the claimYes

Add Labels #

ClaimEffect.add_labels(): adds one or more labels to a claim, and optionally creates new labels before assigning them to the claim.

Parameters #

ParameterTypeDescriptionRequired
labelslist[str or Label]List of label names and Label dataclasses* to apply to the claimYes

*Labels can be passed in by name or as a Label dataclass. If the label with the provided name or values does not exist in your Canvas instance, it will be created and then applied to the specified claim. However, if a label already exists with the provided name or properties, it will add this existing label to the claim.

Label #

The Label dataclass represents a label with specific properties, including color and name.

AttributeTypeDescriptionRequired
colorColorEnumThe color of the label in the UIYes
namestrThe display name of the labelYes

Implementation Details #

  • Validates claim_id is provided and that the associated claim exists.
  • Validates that labels are provided and non-empty.

Example Usage #

from canvas_sdk.effects import Effect
from canvas_sdk.events import EventType
from canvas_sdk.protocols import BaseProtocol

from canvas_sdk.effects.claim import ClaimEffect, Label
from canvas_sdk.v1.data import Note
from canvas_sdk.v1.data.common import ColorEnum


class Protocol(BaseProtocol):
    RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED)

    def compute(self) -> list[Effect]:
        """Creates and adds a new label the claim when charges are pushed.
        Adds the existing Urgent label when the note is locked."""
        note = Note.objects.get(id=self.event.context["note_id"])
        claim = note.get_claim()
        state = self.event.context["state"]
        if state == "PSH":
            claim_effect = ClaimEffect(claim_id=claim.id)
            return [claim_effect.add_labels([Label(color=ColorEnum.PINK, name="pushed not locked")])]
        elif state == "LKD":
            claim_effect = ClaimEffect(claim_id=claim.id)
            return [claim_effect.add_labels(["Urgent"])]

        return []

Remove Labels #

ClaimEffect.remove_labels(): removes existing labels from a claim.

Parameters #

ParameterTypeDescriptionRequired
labelslist[str]List of label names to remove from the claimYes

Implementation Details #

  • Validates claim_id is provided and that the associated claim exists
  • Validates labels is provided and non-empty

Example Usage #

from canvas_sdk.effects import Effect
from canvas_sdk.events import EventType
from canvas_sdk.protocols import BaseProtocol

from canvas_sdk.effects.claim import ClaimEffect
from canvas_sdk.v1.data import Note


class Protocol(BaseProtocol):
    RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED)

    def compute(self) -> list[Effect]:
        """When note is locked, remove the 'pushed not locked' label from the claim."""
        note = Note.objects.get(id=self.event.context["note_id"])
        claim = note.get_claim()
        state = self.event.context["state"]
        if state == "LKD":
            claim_effect = ClaimEffect(claim_id=claim.id)
            return [claim_effect.remove_labels(["pushed not locked"])]
        return []

Move to Queue #

ClaimEffect.move_to_queue(): moves a claim to a specific queue.

Parameters #

ParameterTypeDescriptionRequired
queuestrThe name of the queue to move the claim to, which must be a valid nameYes

Implementation Details #

Example Usage #

from canvas_sdk.effects import Effect
from canvas_sdk.events import EventType
from canvas_sdk.protocols import BaseProtocol
from canvas_sdk.effects.claim import ClaimEffect
from canvas_sdk.v1.data import Note


class Protocol(BaseProtocol):
    RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED)

    def compute(self) -> list[Effect]:
        if self.event.context["state"] == "ULK":
            note = Note.objects.get(id=self.event.context["note_id"])
            claim = note.get_claim()
            claim_effect = ClaimEffect(claim_id=str(claim.id))
            return [claim_effect.move_to_queue("NeedsClinicianReview")]
        return []

Add Comment #

ClaimEffect.add_comment(): creates a new comment on a claim.

Parameters #

ParameterTypeDescriptionRequired
commentstrThe comment text to addYes

Implementation Details #

  • Validates claim_id is provided and that the associated claim exists

Example Usage #

from canvas_sdk.effects import Effect
from canvas_sdk.events import EventType
from canvas_sdk.protocols import BaseProtocol
from canvas_sdk.effects.claim import ClaimEffect
from canvas_sdk.v1.data import Patient, Claim


class Protocol(BaseProtocol):
    RESPONDS_TO = EventType.Name(EventType.COVERAGE_CREATED)

    def compute(self) -> list[Effect]:
        pt = Patient.objects.get(id=self.event.context["patient"]["id"])
        # patient's claims that have not been submitted yet
        pt_claims = Claim.objects.filter(
            note__patient=pt, current_queue__queue_sort_ordering__in=[1, 2, 3, 4]
        )
        return [
            ClaimEffect(claim_id=claim.id).add_comment(
                "Patient has a new coverage, please confirm if this claim's coverage info should be updated."
            )
            for claim in pt_claims
        ]

Post Payment #

ClaimEffect.post_payment(): posts a payment to a claim, specifying payment details and line item transactions. This method supports payments from insurance or patient and allows you to specify payments, adjustments, transfers, and write-offs on individual claim line items.

Parameters #

ParameterTypeDescriptionRequired
claim_coverage_idUUID, str, or 'patient'Identifier for the coverage or the string 'patient' for patient payments.Yes
line_item_transactionslist[LineItemTransaction]List of LineItemTransactions for claim line items.Yes
methodPaymentMethodThe PaymentMethod used (e.g., cash, check, card, other).Yes
move_to_queue_namestrName of the queue to move the claim to after payment.No
claim_descriptionstrDescription for the claim allocation.No
check_datedateDate of the check (required if method is check).No
check_numberstrCheck number (required if method is check).No
deposit_datedateDate the payment was deposited.No
payment_descriptionstrDescription of the payment.No

Validations and Implementation Details #

  • check_number and check_date are required if payment method is check
  • claim_id must correspond to a valid existing claim. For insurance payments, there are a few ways to help you identify the correct claim using the Claim, ClaimSubmission, ClaimCoverage data models:
    • Claim.account_number is the identifier that Canvas sends to the clearinghouse as a unique Canvas identifier for the claim.
    • ClaimSubmission.clearinghouse_claim_id is the identifier that the clearinghouse sends back to Canvas after they have accepted the claim, and is used for the clearinghouse’s internal tracking of the claim.
    • ClaimCoverage.payer_icn is the identifier that the insurance company uses for their internal tracking of the claim, and is usually provided to Canvas via the clearinghouse.
  • claim_coverage_id must be either the string "patient" or correspond to a valid and active ClaimCoverage for the Claim.
    • A helpful way to identify the correct claim coverage is to use the method get_coverage_by_payer_id(payer_id: str, subscriber_number: str | None = None) on the Claim data model, where payer_id is the standard id for the insurance company. You can optionally provide subscriber_number if it’s possible that the patient has multiple coverages from the same payer and you want to identify the correct coverage.
  • move_to_queue_name must be a valid label from ClaimQueue, but is not required. If provided, the claim will move to this queue after payment is applied.

LineItemTransaction #

AttributeTypeDescriptionRequired
claim_line_item_idUUID or strIdentifier for the claim line item.Yes
chargedDecimalCharged amount for the line item.No
allowedDecimalAllowed amount for the line item.No
paymentDecimalPayment amount for the line item.No
adjustmentDecimalAdjustment amount for the line item.No
adjustment_codestrCode describing the adjustment.No
transfer_remaining_balance_toUUID, str, or 'patient'Transfer remaining balance to another payer or patient.No
write_offboolWhether to write off the remaining balance.No
LineItemTransaction Validations #
  • claim_line_item_id must be a valid and active line item for the claim. It is recommended to search for it using .active() and by proc_code, e.g. claim.line_items.active().filter(proc_code="99215").first()
  • There can be many LineItemTransactions for the same claim_line_item_id, but the first LineItemTransaction for a claim line item must specify either a payment or an adjustment (or allowed amount); subsequent transactions require an adjustment.
  • If an adjustment is specified, an adjustment_code must also be provided.
  • If the adjustment code is for a transfer (code starts with “Transfer”), a valid transfer_remaining_balance_to must be provided, and it cannot be the same payer as the claim_coverage_id payer.
  • transfer_remaining_balance_to can only be made to the patient (using the string "patient") or to an active claim_coverage_id for the claim.
  • Adjustments cannot simultaneously write off and transfer the same amount; only one of write_off or transfer_remaining_balance_to should be set on LineItemTransactions where adjustment is present.
  • Adjustments and transfers are not allowed for COPAY charges, i.e. claim line items where the proc_code = COPAY. Only payments are allowed for those line items.
  • payment on COPAY line items must have a claim_coverage_id equal to "patient".
  • allowed should be empty or $0 if claim_coverage_id is equal to "patient".

PaymentMethod Enumeration Type #

EnumValue
CASHcash
CHECKcheck
CARDcard
OTHERother

Example Usage #

The most common use case for this method will be with the SimpleAPI handler.

from canvas_sdk.effects import Effect
from canvas_sdk.v1.data import ClaimLineItem, Claim
from decimal import Decimal
from canvas_sdk.effects.claim import (
    ClaimEffect,
    PaymentMethod,
    LineItemTransaction,
)
from datetime import date
from canvas_sdk.effects.simple_api import JSONResponse, Response
from canvas_sdk.handlers.simple_api import APIKeyCredentials, SimpleAPIRoute


class MyAPI(SimpleAPIRoute):
    PATH = "/routes/post-claim-payment"

    def authenticate(self, credentials: APIKeyCredentials) -> bool:
        # replace with desired authentication logic
        return True

    def get_claim_line_item(self, claim: Claim, proc_code: str) -> ClaimLineItem | None:
        return claim.line_items.active().filter(proc_code=proc_code).first()

    def create_line_item_transactions(
        self, charge: dict, claim: Claim, next_coverage_id: str
    ) -> list[LineItemTransaction]:
        transactions = []
        if not (line_item := self.get_claim_line_item(claim, charge.get("proc_code"))):
            return transactions

        charged = Decimal(charge["charge"])
        payment = Decimal(charge["paid"])
        allowed = Decimal(charge["allowed"])
        adjustments = charge.get("adjustment", [])
        first_adjustment = adjustments[0]
        payment = LineItemTransaction(
            claim_line_item_id=line_item.id,
            charged=charged,
            payment=payment,
            allowed=allowed,
            adjustment=Decimal(first_adjustment["amount"]),
            adjustment_code=f"{first_adjustment['group']}-{first_adjustment['code']}",
            # replace with whatever logic needed for resolving remaining balance
            transfer_remaining_balance_to="patient"
            if first_adjustment["group"] == "PR"
            else next_coverage_id,
        )
        transactions.append(payment)

        additional_adjustments = adjustments[1:]
        for adj in additional_adjustments:
            transaction = LineItemTransaction(
                claim_line_item_id=line_item.id,
                adjustment=Decimal(adj["amount"]),
                adjustment_code=f"{adj['group']}-{adj['code']}",
                # replace with whatever logic needed for resolving remaining balance
                transfer_remaining_balance_to="patient"
                if adj["group"] == "PR"
                else next_coverage_id,
            )
            transactions.append(transaction)

        return transactions

    def get_claim(
        self, account_number: str, clearinghouse_claim_id: str
    ) -> Claim | None:
        return (
            Claim.objects.filter(account_number=account_number).first()
            or Claim.objects.filter(
                submissions__clearinghouse_claim_id=clearinghouse_claim_id,
            ).first()
        )

    def post_payment(
        self,
        claim_payment_info: dict,
        check_number: str,
        check_date: str,
        payer_id: str,
    ) -> Effect | None:
        account_number = claim_payment_info.get("pcn")
        clearinghouse_claim_id = claim_payment_info.get("payer_icn")
        if not (claim := self.get_claim(account_number, clearinghouse_claim_id)):
            return None
        insurance_number = claim_payment_info.get("ins_number")
        if not (coverage := claim.get_coverage_by_payer_id(payer_id, insurance_number)):
            return None

        next_coverage_id = (
            claim.coverages.active().exclude(payer_id=payer_id).first().id
        )

        line_item_transactions = []
        for c in claim_payment_info.get("charge", []):
            line_item_transactions.extend(
                self.create_line_item_transactions(c, claim, next_coverage_id)
            )

        claim_effect = ClaimEffect(claim_id=claim.id)
        return claim_effect.post_payment(
            claim_coverage_id=coverage.id,
            line_item_transactions=line_item_transactions,
            method=PaymentMethod.CHECK,
            check_date=date.fromisoformat(check_date),
            check_number=check_number,
            deposit_date=date.fromisoformat(check_date),
            payment_description="Aetna 835 payment",
            claim_description="Payment applied via 835",
        )

    def post(self) -> list[Response | Effect]:
        payment_info = self.request.json()
        check_number = payment_info.get("check_number")
        check_date = payment_info.get("paid_date")
        payer_id = payment_info.get("payerid")
        payments = [
            p
            for claim in payment_info.get("claim", [])
            if (p := self.post_payment(claim, check_number, check_date, payer_id))
        ]
        return payments + [JSONResponse({"message": "ok"})]

With the above plugin installed, an example call to the endpoint would look like this:


curl -X POST "http://localhost:8000/plugin-io/api/pmt/routes/post-claim-payment" \
  -H "Content-Type: application/json" \
  -H "Authorization: <api-key>" \
  -d '{
    "paid_date": "2025-11-06",
    "eraid": "23853671",
    "check_number": "397547083-1662491258",
    "paid_amount": "346.00",
    "payerid": "60054",
    "claim": [
        {
            "pcn": "124974-1",
            "payer_icn": "TST397547083",
            "total_charge": "48",
            "from_dos": "20250827",
            "pat_name_f": "ETHYL",
            "ins_name_l": "BATES",
            "total_paid": "0",
            "thru_dos": null,
            "pat_name_l": "BATES",
            "ins_number": "412098745",
            "ins_name_f": "NORMAN",
            "charge": [
                {
                    "chgid": "221043771",
                    "from_dos": "20220827",
                    "adjustment": [{"amount": "48", "group": "OA", "code": "109"}],
                    "paid": "0",
                    "allowed": "0",
                    "proc_code": "99212",
                    "charge": "48",
                    "thru_dos": null,
                    "units": "1"
                }
            ]
        },
        {
            "pcn": "21830-1",
            "payer_icn": "TST397547094",
            "total_charge": "75",
            "from_dos": "20220827",
            "pat_name_f": "MARYLOU",
            "ins_name_l": "DENNIS",
            "total_paid": "45",
            "thru_dos": null,
            "pat_name_l": "DENNIS",
            "ins_number": "223444467",
            "ins_name_f": "ROBERT",
            "charge": [
                {
                    "chgid": "221043716",
                    "from_dos": "20220827",
                    "adjustment": [
                        {"amount": "15", "group": "CO", "code": "45"},
                        {"amount": "10", "group": "PR", "code": "2"},
                        {"amount": "5", "group": "PR", "code": "3"}
                    ],
                    "paid": "45",
                    "allowed": "60",
                    "proc_code": "99213",
                    "charge": "75",
                    "thru_dos": null,
                    "units": "1"
                }
            ]
        }
    ]
}'

UpdateClaimLineItem #

The UpdateClaimLineItem effect allows you to update the charge field and linked_diagnosis_codes on a specified claim line item.

Attributes #

AttributeTypeDescriptionRequired
claim_line_item_idUUID or strIdentifier for the claim line itemYes
chargefloatThe charge amount to update on the claim line itemNo
linked_diagnosis_codeslist[UUID or str]List of ClaimLineItemDiagnosisCode IDs to link to the claim line itemNo

Implementation Details #

  • Validates claim_line_item_id is provided and that the associated claim line item exists
  • If linked_diagnosis_codes is provided, validates that all ClaimLineItemDiagnosisCode IDs correspond to existing diagnosis codes on the claim line item
  • The linked_diagnosis_codes list represents the complete set of diagnosis codes that will be linked to the claim line item when the effect is applied. Any diagnosis codes not included in this list will be unlinked. If you wish to add a new code to the existing linked codes, you must first retrieve the current list and include all codes you want to remain linked: list(claim_line_item.diagnosis_codes.filter(linked=True).values_list("id", flat=True)) + [new_code_id]

Example Usage #

Updating charge amount.

from canvas_sdk.effects import Effect
from canvas_sdk.events import EventType
from canvas_sdk.protocols import BaseProtocol
from canvas_sdk.v1.data import Note, ClaimLineItem
from canvas_sdk.effects.claim_line_item import UpdateClaimLineItem


class Protocol(BaseProtocol):
    """When a note is unlocked, update the associated claim's line items to have a charge of $0.00.
    When a note is locked, update the associated claim's line items to have a charge of $500.00."""
    RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED)

    def get_line_items(self) -> ClaimLineItem:
        note = Note.objects.get(id=self.event.context["note_id"])
        claim = note.get_claim()
        return claim.get_active_claim_line_items()

    def update_charge(self, id: str, charge: float) -> Effect:
        return UpdateClaimLineItem(claim_line_item_id=id, charge=charge).apply()

    def update_all_items(self, charge: float) -> list[Effect]:
        return [self.update_charge(line_item.id, charge) for line_item in self.get_line_items()]

    def compute(self) -> list[Effect]:
        if self.event.context["state"] == "ULK":
            return self.update_all_items(0.00)
        if self.event.context["state"] == "LKD":
            return self.update_all_items(500.00)
        return []

Linking and un-linking diagnosis codes.

from canvas_sdk.effects import Effect
from canvas_sdk.events import EventType
from canvas_sdk.protocols import BaseProtocol
from canvas_sdk.v1.data import Note, ClaimLineItem
from canvas_sdk.effects.claim_line_item import UpdateClaimLineItem


class Protocol(BaseProtocol):
    RESPONDS_TO = [
        EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED),
    ]

    def compute(self) -> list[Effect]:
        effects = []
        note = Note.objects.get(id=self.event.context["note_id"])
        if not (claim := note.get_claim()):
            return effects

        state = self.event.context["state"]
        if state == "PSH":
            # only link proc codes starting with "99" to diag codes starting with "I"
            items = claim.line_items.filter(proc_code__startswith="99")
            return self.generate_effects(items, self.get_diags_that_start_with_I)
        if state == "LKD":
            # link all proc codes to all diag codes
            items = claim.line_items.all()
            return self.generate_effects(items, self.get_all_diags)
        if state == "ULK":
            # unlink proc codes starting with "99" from diag codes starting with "I"
            items = claim.line_items.filter(proc_code__startswith="99")
            return self.generate_effects(items, self.get_diags_that_dont_start_with_I)
        if state == "DLT":
            # unlink all proc codes from all diag codes
            items = claim.line_items.all()
            return self.generate_effects(items, self.get_no_diags)

        return effects

    def get_diags_that_start_with_I(self, item: ClaimLineItem) -> list[str]:
        return list(
            item.diagnosis_codes.filter(code__startswith="I").values_list(
                "id", flat=True
            )
        )

    def get_diags_that_dont_start_with_I(self, item: ClaimLineItem) -> list[str]:
        return list(
            item.diagnosis_codes.exclude(code__startswith="I").values_list(
                "id", flat=True
            )
        )

    def get_all_diags(self, item: ClaimLineItem) -> list[str]:
        return list(item.diagnosis_codes.values_list("id", flat=True))

    def get_no_diags(self, item: ClaimLineItem) -> list[str]:
        return []

    def generate_effects(self, items, get_diag_ids) -> list[Effect]:
        return [
            UpdateClaimLineItem(
                claim_line_item_id=item.id, linked_diagnosis_codes=get_diag_ids(item)
            ).apply()
            for item in items
        ]