Saltar al contenido
17 de febrero de 2026·Actualizado el ·19 min de lectura

Patrones de integración Apex: REST, callouts y Named Credentials

Construye integraciones Salesforce fiables — callouts REST con Named Credentials, gestión de límites asíncronos, lógica de reintento con Queueable, Platform Events y mocking de servicios externos.

← Volver

Integrar Salesforce con sistemas externos implica mucho más que hacer una llamada HTTP. Los governor limits, los límites transaccionales, los reintentos y la gestión de secretos requieren atención. Aquí tienes cómo construir integraciones que aguanten en producción.


Requisitos previos

  • Fundamentos de Apex (clases, governor limits)
  • Comprensión de los conceptos básicos de HTTP/REST
  • Un sandbox con una API externa contra la que probar

Named Credentials — nunca hardcodees los endpoints

Las Named Credentials almacenan las URLs de los endpoints y los detalles de autenticación de forma segura, fuera de tu código. Sobreviven a los refresh de sandbox y a las migraciones de org sin necesidad de cambiar código.

Setup → Security → Named Credentials → External Credentials → New

La configuración usa dos objetos — el modelo que Salesforce recomienda desde Winter '23, ya que las Named Credentials «heredadas» están obsoletas:

  1. External Credential Stripe
    • Protocolo de autenticación: Custom
    • Principal: StripePrincipal (Named Principal), con un parámetro de autenticación ApiKey cuyo valor — la clave secreta de Stripe — se introduce únicamente en Setup, nunca en el código ni en el repositorio
    • Cabecera personalizada: Authorization = {!'Bearer ' & $Credential.Stripe.ApiKey}
  2. Named Credential Stripe_API
    • URL: https://api.stripe.com
    • External Credential: Stripe
  3. Permission set: conceder acceso al principal StripePrincipal (External Credential Principal Access) al usuario o proceso que ejecuta el callout
// Usando Named Credentials en los callouts — sin URL ni auth en el código
public class StripeService {
 
    public static Map<String, Object> createCustomer(String email, String name) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:Stripe_API/v1/customers');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
        req.setBody('email=' + EncodingUtil.urlEncode(email, 'UTF-8') +
                    '&name=' + EncodingUtil.urlEncode(name, 'UTF-8'));
 
        Http http = new Http();
        HttpResponse res = http.send(req);
 
        if (res.getStatusCode() == 200) {
            return (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
        }
 
        throw new CalloutException('Stripe API error: ' + res.getStatusCode() + ' ' + res.getBody());
    }
}

Gestionar los governor limits de los callouts

Un callout Apex espera 10 segundos por defecto (120 segundos como máximo, configurable con setTimeout), y el tiempo acumulado de callouts de una transacción está limitado a 120 segundos. Sobre todo, un trigger no puede hacer un callout síncrono: hay que pasar a lo asíncrono.

Patrón: encolar callouts vía Platform Events

// En lugar de llamar a la API externa en un trigger (callout síncrono no permitido),
// publica un Platform Event y procesa de forma asíncrona
 
trigger OpportunityTrigger on Opportunity (after update) {
    List<Integration_Request__e> events = new List<Integration_Request__e>();
 
    for (Opportunity opp : Trigger.New) {
        Opportunity old = Trigger.OldMap.get(opp.Id);
        if (opp.StageName == 'Closed Won' && old.StageName != 'Closed Won') {
            events.add(new Integration_Request__e(
                Record_Id__c = opp.Id,
                Action__c = 'CREATE_DEAL'
            ));
        }
    }
 
    if (!events.isEmpty()) EventBus.publish(events);
}
 
// Un trigger separado en el Platform Event se dispara en su propia transacción.
// Los eventos llegan por lotes (hasta 2.000 por ejecución), que comparten los
// mismos governor limits — y este trigger tampoco puede hacer callouts:
// delega en un Queueable que sí los permite.
trigger IntegrationRequestTrigger on Integration_Request__e (after insert) {
    List<String> recordIds = new List<String>();
    for (Integration_Request__e event : Trigger.New) {
        recordIds.add(event.Record_Id__c);
    }
    System.enqueueJob(new CreateDealQueueable(recordIds));
}
 
public class CreateDealQueueable implements Queueable, Database.AllowsCallouts {
    private List<String> recordIds;
 
    public CreateDealQueueable(List<String> recordIds) {
        this.recordIds = recordIds;
    }
 
    public void execute(QueueableContext ctx) {
        // Como máximo 100 callouts por transacción: más allá, dividir en varios jobs
        for (String recordId : recordIds) {
            ExternalCRMService.createDeal(recordId);
        }
    }
}

Lógica de reintento con backoff exponencial

Las API externas fallan. Construye siempre lógica de reintento para errores transitorios:

// Apex no puede esperar dentro de una transacción: el retraso entre intentos se
// delega en la cola asíncrona, con el parámetro de retraso de System.enqueueJob
// (0 a 10 minutos, desde Spring '23). Primera llamada: new RetryQueueable(id, 1).
public class RetryQueueable implements Queueable, Database.AllowsCallouts {
    private static final Integer MAX_ATTEMPTS = 4;
    // Backoff exponencial: 1, 2 y luego 4 minutos antes de los intentos 2, 3 y 4
    private static final List<Integer> BACKOFF_MINUTES = new List<Integer>{1, 2, 4};
 
    private String recordId;
    private Integer attempt;
 
    public RetryQueueable(String recordId, Integer attempt) {
        this.recordId = recordId;
        this.attempt = attempt;
    }
 
    public void execute(QueueableContext ctx) {
        try {
            ExternalCRMService.createDeal(recordId);
        } catch (Exception e) {
            if (attempt < MAX_ATTEMPTS) {
                Integer delayMinutes = BACKOFF_MINUTES[attempt - 1];
                System.enqueueJob(new RetryQueueable(recordId, attempt + 1), delayMinutes);
            } else {
                // Registrar en un objeto personalizado o enviar una alerta
                ErrorLog__c log = new ErrorLog__c(
                    Record_Id__c = recordId,
                    Error__c = e.getMessage()
                );
                insert log;
            }
        }
    }
}

Parsear respuestas JSON

// Opción 1: deserializeUntyped — flexible pero sin tipado
Map<String, Object> response = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
String customerId = (String) response.get('id');
List<Object> items = (List<Object>) response.get('items');
 
// Opción 2: clase tipada — segura en cuanto a tipos, recomendada para esquemas conocidos
public class StripeCustomer {
    public String id;
    public String email;
    public Long created;
    public StripeAddress address;
 
    public class StripeAddress {
        public String city;
        public String country;
    }
}
 
StripeCustomer customer = (StripeCustomer) JSON.deserialize(res.getBody(), StripeCustomer.class);
String customerId = customer.id;
 
// Opción 3: JSONParser para respuestas muy grandes o complejas
JSONParser parser = JSON.createParser(res.getBody());
while (parser.nextToken() != null) {
    if (parser.getCurrentToken() == JSONToken.FIELD_NAME && parser.getText() == 'id') {
        parser.nextToken();
        String id = parser.getText();
    }
}

Probar los callouts

@isTest
global class StripeApiMock implements HttpCalloutMock {
    private Integer statusCode;
    private String body;
 
    global StripeApiMock(Integer statusCode, String body) {
        this.statusCode = statusCode;
        this.body = body;
    }
 
    global HTTPResponse respond(HTTPRequest req) {
        HTTPResponse res = new HTTPResponse();
        res.setStatusCode(statusCode);
        res.setBody(body);
        res.setHeader('Content-Type', 'application/json');
        return res;
    }
}
 
@isTest
static void testCreateCustomerSuccess() {
    String mockBody = '{"id": "cus_123", "email": "test@example.com"}';
    Test.setMock(HttpCalloutMock.class, new StripeApiMock(200, mockBody));
 
    Test.startTest();
    Map<String, Object> result = StripeService.createCustomer('test@example.com', 'Test User');
    Test.stopTest();
 
    System.assertEquals('cus_123', result.get('id'));
}
 
@isTest
static void testCreateCustomerError() {
    Test.setMock(HttpCalloutMock.class, new StripeApiMock(400, '{"error": {"message": "Invalid email"}}'));
 
    Test.startTest();
    try {
        StripeService.createCustomer('invalid', 'Test');
        System.assert(false, 'Expected exception');
    } catch (CalloutException e) {
        System.assert(e.getMessage().contains('400'));
    }
    Test.stopTest();
}

Errores comunes a evitar

  • Callouts en triggers: los triggers síncronos no pueden hacer callouts — usa @future(callout=true) o un Queueable con Database.AllowsCallouts (un trigger de Platform Event tampoco puede hacer callouts)
  • Secretos en el código: nunca almacenes API keys en clases Apex ni en Custom Metadata — usa Named Credentials
  • Timeout no configurado: el timeout de callout por defecto es de 10s — configúralo siempre explícitamente con req.setTimeout(5000) para las rutas críticas
  • DML antes del callout: Apex bloquea los callouts después de un DML en la misma transacción — reestructura o usa asíncrono

Recursos

Aïcha Imène DAHOUMANE
Sobre la autora
Aïcha Imène DAHOUMANE
Administradora y Desarrolladora Salesforce

Desarrolladora Salesforce con formación en infraestructura IT — construyo soluciones fiables y mantenibles con Apex, Flows, LWC y CI/CD.

CompartirCompartir en LinkedIn