A trigger cannot make a synchronous HTTP callout — the platform throws CalloutException: Callout from triggers are currently not supported. The reasoning is simple: a trigger runs inside the user’s save transaction, and a slow external call would block the database commit and lock other users out. The fix is to call out asynchronously, from a @future(callout=true) method or a Queueable that implements Database.AllowsCallouts.
What fails
// This throws CalloutException
trigger AccountTrigger on Account (after insert) {
for (Account a : Trigger.new) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Stripe/v1/customers');
req.setMethod('POST');
new Http().send(req); // CalloutException
}
}
The future-method pattern
public class StripeService {
@future(callout=true)
public static void createStripeCustomers(Set<Id> accountIds) {
List<Account> accs = [SELECT Id, Name FROM Account WHERE Id IN :accountIds];
for (Account a : accs) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Stripe/v1/customers');
req.setMethod('POST');
req.setBody('name=' + EncodingUtil.urlEncode(a.Name, 'UTF-8'));
HttpResponse resp = new Http().send(req);
// Parse and write back the customer Id
}
}
}
trigger AccountTrigger on Account (after insert) {
Set<Id> ids = Trigger.newMap.keySet();
StripeService.createStripeCustomers(ids);
}
The @future(callout=true) annotation tells the platform two things: queue this method for async execution, and yes, it makes an HTTP callout (so don’t run it inside the current transaction).
The Queueable pattern (preferred in 2026)
Queueable beats @future for callouts because it accepts complex types, can be chained, and returns a JobId you can monitor:
public class StripeSyncJob implements Queueable, Database.AllowsCallouts {
private Set<Id> accountIds;
public StripeSyncJob(Set<Id> ids) { this.accountIds = ids; }
public void execute(QueueableContext ctx) {
// Same callout logic as above
}
}
trigger AccountTrigger on Account (after insert) {
System.enqueueJob(new StripeSyncJob(Trigger.newMap.keySet()));
}
Why pass IDs, not records
@future methods only accept primitive types, Ids, and collections of primitives. They explicitly reject sObject arguments — Salesforce wants you to re-query the records inside the async method so you see the freshest committed state, not a stale snapshot from when the trigger fired.
Queueable doesn’t have that restriction (it accepts complex types), but the convention of passing only Ids is still good practice: smaller payload, no risk of stale field values.
When the callout actually fires
Async work runs after the trigger’s transaction commits. Order of operations:
- Trigger fires; you enqueue the future / queueable.
- Trigger finishes; the platform commits the DML.
- Async job dequeues and runs in a separate transaction.
- Callout goes out.
So the async method sees a fully committed database — including any rows the trigger just inserted.
Common interview follow-ups
- Can a
beforetrigger make a callout? — Same answer: only async. The “before vs after” distinction doesn’t change the rule. - Can I chain
@futurecalls? — No. Future cannot call future. Use Queueable, which supports chaining viaSystem.enqueueJob. - What’s the callout limit? — 100 callouts per transaction, 120 seconds total. Each async job gets its own ceiling.
Verified against: Apex Developer Guide — Invoking Callouts Using Apex, Future Methods. Last reviewed 2026-05-17.