Skip to main content

Revenue Cloud Invoice Write-Offs: Implementation Guide

Overview

Invoice write-offs are a critical billing lifecycle tool in Salesforce Revenue Cloud. A write-off permanently closes an invoice's outstanding balance by creating an auto-applied Credit Memo for the remaining amount. Unlike voiding (which reverses the invoice and resets billing schedules) or manual crediting (which is subject to line-level constraints), write-offs provide a clean financial close-out that preserves the invoice's posted status and leaves billing schedules untouched.

This article covers what the write-off action does mechanically, how to implement it in Apex, when to use it versus other close-out methods, and important behavioral details that affect downstream automation. Whether you are handling bad debt, disputed charges, billing corrections, or invoices stuck in a constraint deadlock, the write-off action is often the right tool for the job.

Revenue Cloud Invoice Write-Offs: Implementation Guide

Invoice Close-Out Methods Compared

Revenue Cloud provides several ways to resolve a posted invoice. Each has different mechanical behavior, downstream effects, and appropriate use cases:

MethodInvoice Status AfterBillingSchedule Reset?Creates Credit Memo?Enables Re-Invoicing?
Credit MemoPostedNoYes (manual)No
VoidVoidedYes (back to ReadyForInvoicing)NoYes (unintentionally)
Write-OffPostedNoYes (automatic)No
Cancel and RebillCanceledDepends on licenseYes + new draftYes (intentional)

The write-off occupies a unique position: it zeroes the balance like a credit but preserves the invoice's posted status and does not disturb billing schedules. This makes it the safest close-out option when re-invoicing is not needed.

What a Write-Off Does

When you write off an invoice, Revenue Cloud:

  1. Creates a Credit Memo for the invoice's remaining balance (not the total -- only the unpaid portion)
  2. Auto-applies the Credit Memo to the invoice's outstanding lines
  3. Sets the invoice balance to $0.00
  4. Updates WriteOffStatus to Completed and populates WriteOffTotalChargeAmount
  5. Leaves Invoice.Status as Posted -- the invoice is not voided or canceled
  6. Does not touch BillingSchedules -- they remain CompletelyBilled

The write-off Credit Memo is a standard CreditMemo record with a ReasonCode value. It appears in reporting and audit trails alongside manually created credit memos.

Prerequisites

CreditMemoReasonCode Standard Value Set

The write-off action requires at least one active value in the CreditMemoReasonCode StandardValueSet. If this picklist is empty, write-off calls fail silently or return an error.

To add a value, deploy metadata or use Setup:

<!-- force-app/main/default/standardValueSets/CreditMemoReasonCode.standardValueSet-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<StandardValueSet xmlns="http://soap.sforce.com/2006/04/metadata">
<sorted>false</sorted>
<standardValue>
<fullName>Correction</fullName>
<default>false</default>
<label>Correction</label>
</standardValue>
<standardValue>
<fullName>Bad Debt</fullName>
<default>false</default>
<label>Bad Debt</label>
</standardValue>
<standardValue>
<fullName>Customer Dispute</fullName>
<default>false</default>
<label>Customer Dispute</label>
</standardValue>
<standardValue>
<fullName>Small Balance</fullName>
<default>false</default>
<label>Small Balance</label>
</standardValue>
</StandardValueSet>

Invoice Must Be Posted

Write-offs only apply to invoices with Status = 'Posted' and a non-zero Balance. Draft, voided, or already-written-off invoices cannot be written off.

Apex Implementation

Using the Standard Action API

Revenue Cloud exposes write-off through the writeOffInvoices standard action. Call it via Apex using the Callable interface or through the REST API.

REST API Approach (from Apex via Named Credential)

public class InvoiceWriteOffService {

private static final String NAMED_CREDENTIAL = 'callout:Self_Org';
private static final String API_VERSION = 'v66.0';

/**
* Writes off one or more invoices using the Revenue Cloud
* writeOffInvoices standard action.
*
* @param invoiceWriteOffs List of write-off requests (invoice ID + reason)
* @return Map of Invoice ID to write-off result (success/failure)
*/
public static Map<Id, WriteOffResult> writeOffInvoices(
List<WriteOffRequest> requests
) {
Map<Id, WriteOffResult> results = new Map<Id, WriteOffResult>();

// Build the request body per the standard action input format
List<Map<String, Object>> writeOffInputs = new List<Map<String, Object>>();
for (WriteOffRequest req : requests) {
Map<String, Object> input = new Map<String, Object>();
input.put('invoiceId', req.invoiceId);
input.put('reasonCode', req.reasonCode);
if (String.isNotBlank(req.reason)) {
input.put('reason', req.reason);
}
writeOffInputs.add(input);
}

Map<String, Object> requestBody = new Map<String, Object>();
requestBody.put('inputs', new List<Map<String, Object>>{
new Map<String, Object>{
'writeOffInvoiceInputs' => writeOffInputs
}
});

HttpRequest httpReq = new HttpRequest();
httpReq.setEndpoint(
NAMED_CREDENTIAL + '/services/data/' +
API_VERSION + '/actions/standard/writeOffInvoices'
);
httpReq.setMethod('POST');
httpReq.setHeader('Content-Type', 'application/json');
httpReq.setBody(JSON.serialize(requestBody));

Http http = new Http();
HttpResponse res = http.send(httpReq);

if (res.getStatusCode() == 200) {
// Parse response and build result map
List<Object> outputValues = (List<Object>) JSON.deserializeUntyped(
res.getBody()
);
for (WriteOffRequest req : requests) {
results.put(req.invoiceId, new WriteOffResult(true, null));
}
} else {
String errorMsg = 'Write-off failed: ' +
res.getStatusCode() + ' - ' + res.getBody();
for (WriteOffRequest req : requests) {
results.put(req.invoiceId, new WriteOffResult(false, errorMsg));
}
}

return results;
}

public class WriteOffRequest {
public Id invoiceId;
public String reasonCode; // Must match CreditMemoReasonCode picklist
public String reason; // Optional free-text description

public WriteOffRequest(Id invoiceId, String reasonCode, String reason) {
this.invoiceId = invoiceId;
this.reasonCode = reasonCode;
this.reason = reason;
}
}

public class WriteOffResult {
public Boolean success;
public String errorMessage;

public WriteOffResult(Boolean success, String errorMessage) {
this.success = success;
this.errorMessage = errorMessage;
}
}
}

Anonymous Apex for Ad-Hoc Write-Offs

For one-off write-offs during testing or manual corrections:

// Quick write-off via Anonymous Apex
HttpRequest req = new HttpRequest();
req.setEndpoint(
URL.getOrgDomainURL().toExternalForm() +
'/services/data/v66.0/actions/standard/writeOffInvoices'
);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
req.setBody(JSON.serialize(new Map<String, Object>{
'inputs' => new List<Map<String, Object>>{
new Map<String, Object>{
'writeOffInvoiceInputs' => new List<Map<String, Object>>{
new Map<String, Object>{
'invoiceId' => '3ttXXXXXXXXXXXXXXX',
'reasonCode' => 'Correction',
'reason' => 'Invoice posted with incorrect coding'
}
}
}
}
}));

Http http = new Http();
HttpResponse res = http.send(req);
System.debug('Status: ' + res.getStatusCode());
System.debug('Body: ' + res.getBody());

Invocable Method for Flow Integration

Expose write-off as a Flow-callable action:

public class InvoiceWriteOffAction {

@InvocableMethod(
label='Write Off Invoice'
description='Writes off an invoice balance using Revenue Cloud standard action'
category='Billing'
)
public static List<Output> writeOff(List<Input> inputs) {
List<Output> outputs = new List<Output>();

for (Input inp : inputs) {
List<InvoiceWriteOffService.WriteOffRequest> requests =
new List<InvoiceWriteOffService.WriteOffRequest>{
new InvoiceWriteOffService.WriteOffRequest(
inp.invoiceId,
inp.reasonCode,
inp.reason
)
};

Map<Id, InvoiceWriteOffService.WriteOffResult> results =
InvoiceWriteOffService.writeOffInvoices(requests);

InvoiceWriteOffService.WriteOffResult result =
results.get(inp.invoiceId);

Output out = new Output();
out.success = result.success;
out.errorMessage = result.errorMessage;
outputs.add(out);
}

return outputs;
}

public class Input {
@InvocableVariable(label='Invoice ID' required=true)
public Id invoiceId;

@InvocableVariable(label='Reason Code' required=true)
public String reasonCode;

@InvocableVariable(label='Reason (optional)')
public String reason;
}

public class Output {
@InvocableVariable(label='Success')
public Boolean success;

@InvocableVariable(label='Error Message')
public String errorMessage;
}
}

Mechanical Behavior in Detail

Understanding exactly what the write-off creates and modifies is critical for downstream automation.

What Gets Created

The write-off action creates a Credit Memo with:

FieldValue
StatusPosted (auto-posted immediately)
Balance$0.00 (auto-applied to the invoice)
ReasonCodeThe value you provided in the request
TotalChargeAmountRemaining invoice balance at time of write-off
ReferenceEntityIdThe written-off Invoice ID

CreditMemoLine records are created only for positive InvoiceLine records that have a remaining balance. Negative invoice lines (such as prepayment reversals) are excluded automatically.

What Gets Updated on the Invoice

FieldBeforeAfter
BalanceRemaining amount$0.00
WriteOffStatusnullCompleted
WriteOffTotalChargeAmountnullAmount written off
NetCreditsAppliedPrevious creditsPrevious + write-off amount
StatusPostedPosted (unchanged)

What Does NOT Change

  • BillingSchedule.Status remains CompletelyBilled -- the write-off does not reset billing schedules
  • Order.Status remains Activated
  • InvoiceLine.Credited_Quantity__c is not updated by write-off credit memo lines (see important note below)

Credited_Quantity__c Gap

This is a critical behavioral detail: Revenue Cloud's InvoiceLine.Credited_Quantity__c field is "maintained by Revenue Cloud credit memo processing," but the write-off action does not update it. This means:

  • Standard credit memos created via the Credit Memo API update Credited_Quantity__c
  • Write-off credit memos do not update Credited_Quantity__c
  • Any rollup automation that reads Credited_Quantity__c to calculate available-to-invoice quantities will not reflect write-off adjustments

If your org relies on Credited_Quantity__c for inventory or billing quantity tracking, you need custom automation to update this field after a write-off.

Business Use Cases

1. Bad Debt Write-Off

An invoice has been outstanding for 90+ days with no expectation of payment. Write off the full balance and record the reason for financial reporting.

new InvoiceWriteOffService.WriteOffRequest(
invoiceId,
'Bad Debt',
'Invoice outstanding 90+ days, customer non-responsive'
);

2. Customer Dispute Resolution

A customer disputes specific charges. After investigation, the business agrees to waive the disputed amount. If a partial credit does not resolve the balance, write off the remainder.

// After applying a partial credit memo for agreed charges,
// write off any remaining disputed balance
new InvoiceWriteOffService.WriteOffRequest(
invoiceId,
'Customer Dispute',
'Remaining balance waived per dispute resolution agreement'
);

3. Small Balance Write-Off

Finance teams often establish a threshold (e.g., under $5.00) below which chasing payment costs more than the amount owed. Automate small-balance write-offs:

// Query invoices with small remaining balances
List<Invoice> smallBalances = [
SELECT Id, Balance
FROM Invoice
WHERE Status = 'Posted'
AND Balance > 0
AND Balance < 5.00
AND WriteOffStatus = null
];

List<InvoiceWriteOffService.WriteOffRequest> requests =
new List<InvoiceWriteOffService.WriteOffRequest>();

for (Invoice inv : smallBalances) {
requests.add(new InvoiceWriteOffService.WriteOffRequest(
inv.Id,
'Small Balance',
'Auto write-off: balance under $5.00 threshold'
));
}

InvoiceWriteOffService.writeOffInvoices(requests);

4. Billing Correction Close-Out

An invoice was posted with incorrect data (wrong coding, wrong pricing, wrong customer reference). The corrected data will go on a new invoice, but the original must be financially closed. Write-off zeroes the balance without voiding or resetting billing schedules.

new InvoiceWriteOffService.WriteOffRequest(
invoiceId,
'Correction',
'Original invoice posted with incorrect JDE coding - replacement invoice pending'
);

5. Negative Line Deadlock Resolution

When an invoice contains negative lines (such as prepayment reversals), standard credit memos cannot fully zero the balance due to constraint conflicts. The write-off action bypasses these constraints because it operates at the invoice level rather than the line level -- it credits only the remaining positive balances and ignores negative lines entirely.

6. Customer Bankruptcy or Account Closure

When a customer files for bankruptcy or closes their account, all outstanding invoices need to be written off as uncollectable. Batch processing handles this efficiently:

// Write off all posted invoices for a closed account
List<Invoice> outstandingInvoices = [
SELECT Id, Balance, BillingAccount.Name
FROM Invoice
WHERE Status = 'Posted'
AND Balance > 0
AND WriteOffStatus = null
AND BillingAccountId = :accountId
];

List<InvoiceWriteOffService.WriteOffRequest> requests =
new List<InvoiceWriteOffService.WriteOffRequest>();

for (Invoice inv : outstandingInvoices) {
requests.add(new InvoiceWriteOffService.WriteOffRequest(
inv.Id,
'Bad Debt',
'Customer account closed - balance uncollectable'
));
}

InvoiceWriteOffService.writeOffInvoices(requests);

Write-Off vs. Void: When to Use Which

ScenarioUse Write-OffUse Void
Close out a bad debtYesNo
Correct and re-invoiceNoMaybe
Resolve a negative-line deadlockYesNo
Cancel an invoice entirelyNoYes
Preserve billing schedule statusYesNo
Need the balance at $0 with audit trailYesNo
Need to regenerate the invoice from scratchNoYes

General rule: Use write-off when you want to close out an invoice financially but do not need to re-invoice. Use void when you need to undo the invoice entirely and regenerate it (accepting that billing schedules will reset).

Write-Off Interaction with "Convert Negative Invoice Lines" Setting

Revenue Cloud's "Convert Negative Invoice Lines to Credit Memo Lines" billing setting automatically creates a Credit Memo from negative InvoiceLines at post time. When this setting is enabled:

  • The negative line remains on the invoice
  • An auto-Credit Memo neutralizes the negative line's financial impact
  • The invoice balance reflects only the positive lines minus the auto-credit

If a write-off is then performed on this invoice, the write-off Credit Memo covers only the remaining balance (positive lines minus any previously applied credits, including the auto-credit). The two credit memos (auto-convert and write-off) coexist without conflict.

Key Takeaways

  • Write-off creates an auto-applied Credit Memo that zeroes the invoice balance while keeping the invoice in Posted status.
  • It requires at least one active CreditMemoReasonCode picklist value.
  • BillingSchedules are not reset -- written-off invoices cannot be re-invoiced through standard means.
  • InvoiceLine.Credited_Quantity__c is not updated by write-off credit memos -- plan for this gap if your org uses quantity-based rollups.
  • Write-off handles negative invoice lines gracefully by crediting only positive remaining balances.
  • Use write-off for financial close-outs (bad debt, disputes, corrections, small balances). Use void only when re-invoicing is required.