Skip to main content

SF-0332 · Scenario · Medium

Can a trigger make a call to the apex callout method?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

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:

  1. Trigger fires; you enqueue the future / queueable.
  2. Trigger finishes; the platform commits the DML.
  3. Async job dequeues and runs in a separate transaction.
  4. 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 before trigger make a callout? — Same answer: only async. The “before vs after” distinction doesn’t change the rule.
  • Can I chain @future calls? — No. Future cannot call future. Use Queueable, which supports chaining via System.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.