If you’ve ever tried to run an Apex trigger callout, you’ve likely run into that brick wall of an error message. Salesforce doesn’t allow a synchronous Apex trigger callout because it puts the whole database transaction at risk of timing out while waiting for an external server to respond.
Here’s the thing: triggers are fast, but APIs are slow. If the external server takes five seconds to respond, your Salesforce record is locked for those five seconds. Multiply that by 200 records in a bulk update, and you’ve got a disaster on your hands. So, how do we get around this? We go asynchronous.
Why you can’t run a synchronous Apex trigger callout
Salesforce triggers execute as part of the database transaction. If you try to make an HTTP request directly inside that trigger, the platform stops you. It’s a safety mechanism. External latency is unpredictable, and Salesforce wants to keep its database transactions tight and reliable. If the callout hangs, the transaction hangs, and eventually, everything rolls back.
But don’t worry, there are a few ways to handle this without breaking the rules. You just have to change how you think about the timing of the request. Instead of doing it “now,” you tell Salesforce to do it “as soon as you have a moment.”
Pro approaches for an Apex trigger callout
1) Using @future(callout=true)
This is the classic, fire-and-forget method. You create a static method, mark it as a future method, and tell it that callouts are allowed. It’s perfect for simple integrations where you don’t need to do much with the result immediately. Just remember that you need to stay under asynchronous Apex limits while doing this.
public class CalloutService {
@future(callout=true)
public static void doCallout(String jsonPayload) {
Http http = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/endpoint');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(jsonPayload);
HttpResponse res = http.send(req);
// Log your response here
}
}
// Trigger snippet
trigger AccountTrigger on Account (after insert) {
List<String> payloads = new List<String>();
for (Account a : Trigger.new) {
payloads.add(JSON.serialize(a));
}
CalloutService.doCallout(JSON.serialize(payloads));
}2) Queueable Apex (My personal favorite)
If you need more control, Queueable is the way to go. It’s like a future method but better because it supports complex data types and allows for job chaining. I use this most of the time because it’s easier to track and debug. You’ll need to use Database.AllowsCallouts to make it work.
public class QueueableCallout implements Queueable, Database.AllowsCallouts {
private List<String> payloads;
public QueueableCallout(List<String> payloads){
this.payloads = payloads;
}
public void execute(QueueableContext ctx){
Http http = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_Named_Credential/endpoint');
req.setMethod('POST');
req.setBody(JSON.serialize(payloads));
HttpResponse res = http.send(req);
}
}
// Trigger snippet
trigger OpportunityTrigger on Opportunity (after insert) {
List<String> payloads = new List<String>();
for (Opportunity o : Trigger.new) payloads.add(JSON.serialize(o));
System.enqueueJob(new QueueableCallout(payloads));
}3) Platform Events
This is the most modern way to handle an Apex trigger callout. You publish an event from your trigger, and an event-based trigger or an external system picks it up. This completely decouples the process. It’s a bit more setup, but it’s incredibly scalable compared to standard outbound messages.
One thing that trips people up: never put your callout logic inside a loop in your trigger. Even if it’s an async call, you’ll hit governor limits fast. Always collect your data first and send it in one big batch.
Best practices for managing your Apex trigger callout
- Named Credentials: Don’t hardcode URLs or API keys. Use Named Credentials. It makes authentication so much easier to manage across different environments.
- Bulkification: I can’t stress this enough. If 200 records are updated at once, your trigger should only fire one async job, not 200.
- Error Handling: Since the callout happens in the background, you won’t see the error on the record page. You need to log failures to a custom object so you actually know when something goes wrong.
- Testing: You can’t make real callouts in tests. You’ll need to implement
HttpCalloutMock. For more on building reliable integrations, check out this Salesforce API integration guide.
How to mock and test an Apex trigger callout
@IsTest
global class MockHttpResponseGenerator implements HttpCalloutMock {
global HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type','application/json');
res.setBody('{"status":"success"}');
res.setStatusCode(200);
return res;
}
}
@IsTest
private class CalloutServiceTest {
@IsTest static void testAsyncCallout() {
Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());
Test.startTest();
CalloutService.doCallout('{"key":"value"}');
Test.stopTest();
// Check your logs or custom error object here
}
}Key Takeaways
- Synchronous callouts are forbidden in triggers to prevent transaction timeouts.
- Use
@future(callout=true)for simple, quick tasks. - Use Queueable Apex for complex logic or when you need to chain jobs together.
- Always bulkify your requests to avoid hitting concurrent async limits.
- Named Credentials are your best friend for security and maintenance.
So, the short answer? You can’t make a callout directly, but with a little async magic, you can get the job done. Start with Queueable if you’re unsure – it’s usually the most flexible choice for any real-world project. Just make sure you’re logging those responses so you aren’t flying blind when an API goes down.








Leave a Reply