Creating Revenue Cloud Credit Memos Programmatically via REST API
Revenue Cloud does not provide a standard Apex DML path for creating Credit Memos. Instead, Credit Memos are created through the Commerce Invoicing REST API, which means even Apex code running inside the same org must make an HTTP callout to itself. This pattern requires Named Credentials for authentication, specific request formatting, and awareness of how the API handles responses.
Named Credentials for Self-Org Callouts
Making an HTTP callout from a Salesforce org to itself requires authentication. Hardcoding the org's URL and session ID is fragile and insecure. Named Credentials provide a managed, declarative solution.
Setup requirements:
- Create an External Credential with an OAuth 2.0 authentication protocol (Client Credentials flow works well for server-to-server within the same org).
- Create a Named Credential that references the External Credential, with the URL set to your org's My Domain URL (e.g.,
https://acme-corp.my.salesforce.com). - Assign the appropriate Permission Set to grant the running user access to the External Credential's Principal.
The Named Credential handles token management, refresh, and endpoint resolution automatically.
API Endpoint
The Credit Memo creation endpoint follows this structure:
POST /services/data/v62.0/commerce/invoicing/credit-memo
Replace v62.0 with the API version appropriate for your org.
Complete Apex Implementation
public class CreditMemoService {
private static final String NAMED_CREDENTIAL = 'callout:Acme_Self_Org';
private static final String API_VERSION = 'v62.0';
public static Id createCreditMemo(
Id invoiceId,
List<CreditMemoLineInput> lineInputs
) {
// Build the request body
Map<String, Object> requestBody = new Map<String, Object>();
requestBody.put('invoiceId', invoiceId);
List<Map<String, Object>> creditMemoLines = new List<Map<String, Object>>();
for (CreditMemoLineInput lineInput : lineInputs) {
Map<String, Object> line = new Map<String, Object>();
line.put('invoiceLineId', lineInput.invoiceLineId);
line.put('quantity', lineInput.quantity);
line.put('amount', lineInput.amount);
line.put('taxAmount', lineInput.taxAmount);
line.put('taxStrategy', lineInput.taxStrategy);
line.put('description', lineInput.description);
creditMemoLines.add(line);
}
requestBody.put('creditMemoLines', creditMemoLines);
// Make the callout
HttpRequest req = new HttpRequest();
req.setEndpoint(
NAMED_CREDENTIAL + '/services/data/' +
API_VERSION + '/commerce/invoicing/credit-memo'
);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(requestBody));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() != 200 && res.getStatusCode() != 201) {
throw new CreditMemoException(
'Credit Memo creation failed: ' +
res.getStatusCode() + ' - ' + res.getBody()
);
}
// Query for the newly created Credit Memo
// The API creates the record synchronously despite
// the response format suggesting async processing
List<CreditMemo> memos = [
SELECT Id, DocumentNumber, TotalAmount, TotalTaxAmount
FROM CreditMemo
WHERE ReferenceEntityId = :invoiceId
ORDER BY CreatedDate DESC
LIMIT 1
];
if (memos.isEmpty()) {
throw new CreditMemoException(
'Credit Memo created via API but could not be queried.'
);
}
return memos[0].Id;
}
public class CreditMemoLineInput {
public Id invoiceLineId;
public Decimal quantity;
public Decimal amount;
public Decimal taxAmount;
public String taxStrategy; // ManualOverride, CopyFromInvoiceLine, Ignore
public String description;
}
public class CreditMemoException extends Exception {}
}
API Response Behavior
The Commerce Invoicing API returns a response that resembles an asynchronous operation -- it may include a reference ID or status indicator. However, the Credit Memo record is created synchronously. By the time you receive the HTTP response, the CreditMemo and its CreditMemoLine records exist in the database and are immediately queryable.
This is important because it means you do not need to poll for completion. You can query for the Credit Memo immediately after a successful response.
Cascading Updates After Creation
Creating the Credit Memo is only the first step. Downstream records typically need updating to reflect the credit:
InvoiceLine Updates
Track how much of each Invoice Line has been credited to prevent over-crediting:
// After Credit Memo creation, update credited quantities
List<InvoiceLine> linesToUpdate = new List<InvoiceLine>();
for (CreditMemoLineInput lineInput : lineInputs) {
InvoiceLine il = invoiceLineMap.get(lineInput.invoiceLineId);
// Assuming a custom field to track credited quantity
il.Quantity_Credited__c =
(il.Quantity_Credited__c != null ? il.Quantity_Credited__c : 0)
+ lineInput.quantity;
linesToUpdate.add(il);
}
update linesToUpdate;
OrderItem Updates
If the credit corresponds to a quantity or billing adjustment on the original Order:
// Update OrderItem to reflect the credited quantity
List<OrderItem> itemsToUpdate = new List<OrderItem>();
for (OrderItem oi : affectedOrderItems) {
oi.Quantity_Credited__c =
(oi.Quantity_Credited__c != null ? oi.Quantity_Credited__c : 0)
+ creditedQuantity;
itemsToUpdate.add(oi);
}
update itemsToUpdate;
Key Considerations
| Concern | Guidance |
|---|---|
| Authentication | Always use Named Credentials; never hardcode session IDs or org URLs |
| API version | Use a version that supports Commerce Invoicing (v55.0+) |
| Callout context | Cannot make callouts from triggers; use Queueable or @future(callout=true) |
| Error handling | Parse the response body for detailed error messages; log failures for investigation |
| Idempotency | The API does not enforce idempotency; implement your own duplicate prevention logic |
| Governor limits | Each callout counts against the 100-callout-per-transaction limit |
The self-org callout pattern may feel unusual, but it is the supported path for programmatic Credit Memo creation in Revenue Cloud. Named Credentials make it secure and maintainable.