Step 3 - Create Apex Class (Document AI API Wrapper)

Create a custom Apex class to enable the Document AI flow functionality. This Apex class:

  • Allows the Salesforce flow to invoke the Data 360 Document AI functionality
  • Passes in necessary credentials and document identities
  • Receives a clean JSON output of the extracted bank statement data
  1. Go to the Setup Menu.
  2. From the dropdown menu, select Developer Console.
  3. In the Developer Console, click File > New > Apex Class.
  4. Enter the class name: de_BankStatementExtractor and click OK.
  5. When the code editor opens, paste this code into the editor and save the file.

This is an out-of-the-box class that includes a method invoked by the flow you create on the next step. If you choose to use a different Apex class, make sure it includes such an invocable method. This class also contains a hardcoded override for the LLM model. If you use a different model, update the llmModel string.

Note

1public class de_BankStatementExtractor {
2
3    @InvocableMethod(label='Run Bank statement analysis using DataCloud Document AI')
4    public static List<Result> runImageProcessing(List<Request> requests) {
5        List<Result> results = new List<Result>();
6
7        for (Request req : requests) {
8            Result result = new Result();
9            try {
10                // 1. Get access token
11                String token = getAccessToken(req.Org_Domain, req.Client_Id, req.Client_Secret);
12                System.debug('Access Token >> ' + token);
13
14                //  Intialize variables
15                String orgDomain   = req.Org_Domain;
16                String llmModel    = req.LLM_Model;
17                String idpConfigId = req.IDPConfigId;
18
19                // 2. Get latest version ID as fileId
20                ContentVersion cv = [
21                    SELECT Id
22                    FROM ContentVersion
23                    WHERE ContentDocumentId = :req.ContentDocumentId
24                    ORDER BY VersionNumber DESC
25                    LIMIT 1
26                ];
27                String fileId = cv.Id;
28                System.debug('Using fileId >> ' + fileId);
29
30                // 3. Call schema generation or extract directly
31                String schemaJson;
32                String finalResponse;
33
34                if (String.isBlank(idpConfigId)) {
35                    schemaJson = getSchemaJson(token, fileId, orgDomain, llmModel);
36                    System.debug('Decoded Schema JSON >> ' + schemaJson);
37
38                    finalResponse = extractData(token, fileId, schemaJson, orgDomain, llmModel, null);
39                } else {
40                    finalResponse = extractData(token, fileId, null, orgDomain, llmModel, idpConfigId);
41                }
42
43                result.Status = 'Success';
44                result.ExtractedDataJson = finalResponse;
45
46            } catch (Exception e) {
47                result.Status = 'Failed';
48                result.ErrorMessage = e.getMessage();
49                System.debug('Error >> ' + e.getMessage());
50            }
51            results.add(result);
52        }
53        return results;
54    }
55
56    public class Request {
57        @InvocableVariable(label='Org Domain' required=true) public String Org_Domain;
58        @InvocableVariable(label='Client Id' required=true) public String Client_Id;
59        @InvocableVariable(label='Client Secret' required=true) public String Client_Secret;
60        @InvocableVariable(label='Document Type' required=true) public String Document_Type;
61        @InvocableVariable(label='LLM Model') public String LLM_Model;
62        @InvocableVariable(label='Content Document Id' required=true) public Id ContentDocumentId;
63        @InvocableVariable(label='IDPConfigId') public String IDPConfigId;
64    }
65    public class Result {
66        @InvocableVariable(label='Status') public String Status;
67        @InvocableVariable(label='Extracted Data (JSON)') public String ExtractedDataJson;
68        @InvocableVariable(label='Error Message (if any)') public String ErrorMessage;
69    }
70
71    // === 1. Get access token ===
72    private static String getAccessToken(String orgDomain, String clientId, String clientSecret) {
73        HttpRequest req = new HttpRequest();
74        req.setEndpoint(orgDomain + '/services/oauth2/token');
75        req.setMethod('POST');
76        req.setHeader('Content-Type','application/x-www-form-urlencoded');
77        req.setBody('grant_type=client_credentials&client_id=' + clientId + '&client_secret=' + clientSecret);
78
79        HttpResponse res = new Http().send(req);
80        if (res.getStatusCode() == 200) {
81            Map<String,Object> resp = (Map<String,Object>)JSON.deserializeUntyped(res.getBody());
82            return (String)resp.get('access_token');
83        }
84        throw new CalloutException('Access Token Error: '+res.getBody());
85    }
86
87    // === 2. Generate schema ===
88    private static String getSchemaJson(String accessToken, String fileId, String orgDomain, String llmModel) {
89        HttpRequest req = new HttpRequest();
90        req.setEndpoint(orgDomain + '/services/data/v63.0/ssot/document-processing/actions/generate-schema');
91        req.setMethod('POST');
92        req.setHeader('Authorization','Bearer '+accessToken);
93        req.setHeader('Content-Type','application/json');
94        req.setHeader('Accept','application/json');
95        req.setHeader('Sforce-Call-Options','client=SchemaTest');
96        req.setTimeout(120000);
97
98        // Force hardcoded model for testing
99        llmModel = 'llmgateway__OpenAIGPT4Omni_08_06';
100
101        Map<String,Object> payload = new Map<String,Object>{
102            'mlModel' => llmModel,
103            'files'   => new List<Object>{ new Map<String,Object>{ 'fileId' => fileId } }
104        };
105
106        String body = JSON.serialize(payload);
107        System.debug('Sending Schema Payload: ' + body);
108
109        req.setBody(body);
110
111        HttpResponse res = new Http().send(req);
112        System.debug('Schema Status Code: ' + res.getStatusCode());
113        System.debug('Schema Response: ' + res.getBody());
114
115        if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
116            Map<String,Object> bodyMap = (Map<String,Object>) JSON.deserializeUntyped(res.getBody());
117            String schemaRaw = (String) bodyMap.get('schema');
118            if (schemaRaw == null) {
119                throw new CalloutException('Schema Error: No "schema" field in response');
120            }
121            // Decode HTML escapes
122            String schemaDecoded = schemaRaw.replaceAll('&quot;', '"').replaceAll('&amp;', '&');
123            return schemaDecoded;
124        } else {
125            throw new CalloutException('Schema Error(' + res.getStatusCode() + '): ' + res.getBody());
126        }
127    }
128
129    // === 3. Extract data ===
130    private static String extractData(String accessToken, String fileId, String schemaJson,
131                                      String orgDomain, String llmModel, String idpConfigId) {
132        HttpRequest req = new HttpRequest();
133        req.setEndpoint(orgDomain + '/services/data/v63.0/ssot/document-processing/actions/extract-data');
134        req.setMethod('POST');
135        req.setHeader('Authorization','Bearer '+accessToken);
136        req.setHeader('Content-Type','application/json');
137        req.setHeader('Accept','application/json');
138        req.setTimeout(120000);
139
140        Map<String,Object> payload;
141        if (!String.isBlank(schemaJson)) {
142            payload = new Map<String,Object>{
143                'mlModel'=>llmModel,
144                'files'=>new List<Object>{ new Map<String,Object>{ 'fileId'=>fileId } },
145                'schemaConfig'=>schemaJson
146            };
147        } else {
148            payload = new Map<String,Object>{
149                'idpConfigurationIdOrName'=>idpConfigId,
150                'files'=>new List<Object>{ new Map<String,Object>{ 'fileId'=>fileId } }
151            };
152        }
153
154        System.debug('Extract Payload >> '+JSON.serializePretty(payload));
155        req.setBody(JSON.serialize(payload));
156
157        HttpResponse res = new Http().send(req);
158        System.debug('Extract Status >> '+res.getStatusCode());
159        System.debug('Extract Response >> '+res.getBody());
160
161        if (res.getStatusCode()>=200 && res.getStatusCode()<300) {
162            Map<String,Object> bodyMap = (Map<String,Object>)JSON.deserializeUntyped(res.getBody());
163            if (bodyMap.containsKey('data')) {
164                return JSON.serialize(bodyMap.get('data'));
165            }
166            return 'No data field in response';
167        }
168        throw new CalloutException('Extract Error('+res.getStatusCode()+'): '+res.getBody());
169    }
170}

Next Step