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.

note: 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.

public class de_BankStatementExtractor {

    @InvocableMethod(label='Run Bank statement analysis using DataCloud Document AI')
    public static List<Result> runImageProcessing(List<Request> requests) {
        List<Result> results = new List<Result>();

        for (Request req : requests) {
            Result result = new Result();
            try {
                // 1. Get access token
                String token = getAccessToken(req.Org_Domain, req.Client_Id, req.Client_Secret);
                System.debug('Access Token >> ' + token);

                //  Intialize variables
                String orgDomain   = req.Org_Domain;
                String llmModel    = req.LLM_Model;
                String idpConfigId = req.IDPConfigId;

                // 2. Get latest version ID as fileId
                ContentVersion cv = [
                    SELECT Id
                    FROM ContentVersion
                    WHERE ContentDocumentId = :req.ContentDocumentId
                    ORDER BY VersionNumber DESC
                    LIMIT 1
                ];
                String fileId = cv.Id;
                System.debug('Using fileId >> ' + fileId);

                // 3. Call schema generation or extract directly
                String schemaJson;
                String finalResponse;

                if (String.isBlank(idpConfigId)) {
                    schemaJson = getSchemaJson(token, fileId, orgDomain, llmModel);
                    System.debug('Decoded Schema JSON >> ' + schemaJson);

                    finalResponse = extractData(token, fileId, schemaJson, orgDomain, llmModel, null);
                } else {
                    finalResponse = extractData(token, fileId, null, orgDomain, llmModel, idpConfigId);
                }

                result.Status = 'Success';
                result.ExtractedDataJson = finalResponse;

            } catch (Exception e) {
                result.Status = 'Failed';
                result.ErrorMessage = e.getMessage();
                System.debug('Error >> ' + e.getMessage());
            }
            results.add(result);
        }
        return results;
    }

    public class Request {
        @InvocableVariable(label='Org Domain' required=true) public String Org_Domain;
        @InvocableVariable(label='Client Id' required=true) public String Client_Id;
        @InvocableVariable(label='Client Secret' required=true) public String Client_Secret;
        @InvocableVariable(label='Document Type' required=true) public String Document_Type;
        @InvocableVariable(label='LLM Model') public String LLM_Model;
        @InvocableVariable(label='Content Document Id' required=true) public Id ContentDocumentId;
        @InvocableVariable(label='IDPConfigId') public String IDPConfigId;
    }
    public class Result {
        @InvocableVariable(label='Status') public String Status;
        @InvocableVariable(label='Extracted Data (JSON)') public String ExtractedDataJson;
        @InvocableVariable(label='Error Message (if any)') public String ErrorMessage;
    }

    // === 1. Get access token ===
    private static String getAccessToken(String orgDomain, String clientId, String clientSecret) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(orgDomain + '/services/oauth2/token');
        req.setMethod('POST');
        req.setHeader('Content-Type','application/x-www-form-urlencoded');
        req.setBody('grant_type=client_credentials&client_id=' + clientId + '&client_secret=' + clientSecret);

        HttpResponse res = new Http().send(req);
        if (res.getStatusCode() == 200) {
            Map<String,Object> resp = (Map<String,Object>)JSON.deserializeUntyped(res.getBody());
            return (String)resp.get('access_token');
        }
        throw new CalloutException('Access Token Error: '+res.getBody());
    }

    // === 2. Generate schema ===
    private static String getSchemaJson(String accessToken, String fileId, String orgDomain, String llmModel) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(orgDomain + '/services/data/v63.0/ssot/document-processing/actions/generate-schema');
        req.setMethod('POST');
        req.setHeader('Authorization','Bearer '+accessToken);
        req.setHeader('Content-Type','application/json');
        req.setHeader('Accept','application/json');
        req.setHeader('Sforce-Call-Options','client=SchemaTest');
        req.setTimeout(120000);

        // Force hardcoded model for testing
        llmModel = 'llmgateway__OpenAIGPT4Omni_08_06';

        Map<String,Object> payload = new Map<String,Object>{
            'mlModel' => llmModel,
            'files'   => new List<Object>{ new Map<String,Object>{ 'fileId' => fileId } }
        };

        String body = JSON.serialize(payload);
        System.debug('Sending Schema Payload: ' + body);

        req.setBody(body);

        HttpResponse res = new Http().send(req);
        System.debug('Schema Status Code: ' + res.getStatusCode());
        System.debug('Schema Response: ' + res.getBody());

        if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
            Map<String,Object> bodyMap = (Map<String,Object>) JSON.deserializeUntyped(res.getBody());
            String schemaRaw = (String) bodyMap.get('schema');
            if (schemaRaw == null) {
                throw new CalloutException('Schema Error: No "schema" field in response');
            }
            // Decode HTML escapes
            String schemaDecoded = schemaRaw.replaceAll('&quot;', '"').replaceAll('&amp;', '&');
            return schemaDecoded;
        } else {
            throw new CalloutException('Schema Error(' + res.getStatusCode() + '): ' + res.getBody());
        }
    }

    // === 3. Extract data ===
    private static String extractData(String accessToken, String fileId, String schemaJson,
                                      String orgDomain, String llmModel, String idpConfigId) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(orgDomain + '/services/data/v63.0/ssot/document-processing/actions/extract-data');
        req.setMethod('POST');
        req.setHeader('Authorization','Bearer '+accessToken);
        req.setHeader('Content-Type','application/json');
        req.setHeader('Accept','application/json');
        req.setTimeout(120000);

        Map<String,Object> payload;
        if (!String.isBlank(schemaJson)) {
            payload = new Map<String,Object>{
                'mlModel'=>llmModel,
                'files'=>new List<Object>{ new Map<String,Object>{ 'fileId'=>fileId } },
                'schemaConfig'=>schemaJson
            };
        } else {
            payload = new Map<String,Object>{
                'idpConfigurationIdOrName'=>idpConfigId,
                'files'=>new List<Object>{ new Map<String,Object>{ 'fileId'=>fileId } }
            };
        }

        System.debug('Extract Payload >> '+JSON.serializePretty(payload));
        req.setBody(JSON.serialize(payload));

        HttpResponse res = new Http().send(req);
        System.debug('Extract Status >> '+res.getStatusCode());
        System.debug('Extract Response >> '+res.getBody());

        if (res.getStatusCode()>=200 && res.getStatusCode()<300) {
            Map<String,Object> bodyMap = (Map<String,Object>)JSON.deserializeUntyped(res.getBody());
            if (bodyMap.containsKey('data')) {
                return JSON.serialize(bodyMap.get('data'));
            }
            return 'No data field in response';
        }
        throw new CalloutException('Extract Error('+res.getStatusCode()+'): '+res.getBody());
    }
}

Next Step