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:
| Method | Invoice Status After | BillingSchedule Reset? | Creates Credit Memo? | Enables Re-Invoicing? |
|---|---|---|---|---|
| Credit Memo | Posted | No | Yes (manual) | No |
| Void | Voided | Yes (back to ReadyForInvoicing) | No | Yes (unintentionally) |
| Write-Off | Posted | No | Yes (automatic) | No |
| Cancel and Rebill | Canceled | Depends on license | Yes + new draft | Yes (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:
- Creates a Credit Memo for the invoice's remaining balance (not the total -- only the unpaid portion)
- Auto-applies the Credit Memo to the invoice's outstanding lines
- Sets the invoice balance to $0.00
- Updates
WriteOffStatustoCompletedand populatesWriteOffTotalChargeAmount - Leaves Invoice.Status as
Posted-- the invoice is not voided or canceled - 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:
| Field | Value |
|---|---|
Status | Posted (auto-posted immediately) |
Balance | $0.00 (auto-applied to the invoice) |
ReasonCode | The value you provided in the request |
TotalChargeAmount | Remaining invoice balance at time of write-off |
ReferenceEntityId | The 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
| Field | Before | After |
|---|---|---|
Balance | Remaining amount | $0.00 |
WriteOffStatus | null | Completed |
WriteOffTotalChargeAmount | null | Amount written off |
NetCreditsApplied | Previous credits | Previous + write-off amount |
Status | Posted | Posted (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__cto 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
| Scenario | Use Write-Off | Use Void |
|---|---|---|
| Close out a bad debt | Yes | No |
| Correct and re-invoice | No | Maybe |
| Resolve a negative-line deadlock | Yes | No |
| Cancel an invoice entirely | No | Yes |
| Preserve billing schedule status | Yes | No |
| Need the balance at $0 with audit trail | Yes | No |
| Need to regenerate the invoice from scratch | No | Yes |
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
CreditMemoReasonCodepicklist value. - BillingSchedules are not reset -- written-off invoices cannot be re-invoiced through standard means.
InvoiceLine.Credited_Quantity__cis 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.