Salesforce Apex HTTP callout: POST JSON to an external API

An Apex HTTP callout builds an HttpRequest, sends it with Http.send and reads the HttpResponse. From a trigger or after DML, it must run asynchronously.

5 min readSume
All posts

A Salesforce Apex HTTP callout sends an HTTP request from Apex code and reads the response: build an HttpRequest, set the endpoint, method, headers and body, send it with Http.send, and check getStatusCode() on the HttpResponse. The endpoint must be authorized first, through Remote Site Settings or a named credential, and a callout from a trigger or after DML must run in a future method or Queueable Apex.

Salesforce facts come from Trailhead's units on callouts, REST callouts, future methods and protecting secrets, plus Salesforce's Apex Recipes sample for the named-credential endpoint. Sume facts come from Create a run, Runs and results and Authentication. All were read on 2026-09-29. Sume has no Salesforce package; this is a plain HTTPS callout.

Where do I keep the API key?

In a named credential, not in code. Salesforce manages the authentication for callouts that name a named credential as their endpoint, and you can change it in settings without touching the code that refers to it. An external credential holds the authentication details, and custom headers can be added to both, which is where an x-api-key header goes. In Apex the endpoint then starts with callout: and the named credential's name, as Apex Recipes builds it.

Sume accepts Authorization: Bearer or x-api-key. Send one: a request carrying both is refused with 401 unauthorized.

How do I POST JSON from Apex without breaking the transaction?

Put the callout in a @Future(callout=true) method and pass record ids, since future methods take only primitive types or collections of them. Trailhead advises bundling callouts into one future method rather than one method per callout. The Sume call below starts one Format run per Opportunity; each create answers 202 Accepted with a run receipt while the video renders.

public with sharing class SumeRunStarter {
  @Future(callout=true)
  public static void startRuns(List<Id> opportunityIds) {
    Http http = new Http();
    for (Id oppId : opportunityIds) {
      HttpRequest request = new HttpRequest();
      request.setEndpoint('callout:Sume/v1/formats/acme/product-promo/runs');
      request.setMethod('POST');
      request.setHeader('Content-Type', 'application/json');
      request.setHeader('Idempotency-Key', 'opp-' + oppId + '-promo-v1');
      request.setBody(JSON.serialize(new Map<String, Object>{
        'instruction' => '15-second vertical promo for this deal.',
        'input' => new Map<String, Object>{ 'opportunity_id' => oppId },
        'generation_spend_cap_usd' => 20,
        'communication' => new Map<String, Object>{
          'webhook_url' => 'https://example.com/hooks/sume' }
      }));
      HttpResponse response = http.send(request);
      Integer code = response.getStatusCode();
      if (code != 202 && code != 200) {
        System.debug('Sume answered ' + code + ': ' + response.getBody());
      }
    }
  }
}

Why is a callout not allowed from my trigger?

Because a synchronous callout in a trigger would hold the database connection open for the whole call. Trailhead's rules for where a callout can run:

From Trailhead's callouts, REST callouts and future methods units, read 2026-09-29.
SituationWhat Salesforce requires
Callout from a trigger or after DMLA future method or the Queueable interface
Any external endpointAuthorized in Remote Site Settings, or reached through a named credential
Future method argumentsPrimitive types or collections of them, such as a list of record ids
Order of future methodsNot guaranteed; two can run at the same time
Apex testsNo real callouts; use HttpCalloutMock or StaticResourceCalloutMock

How do I avoid paying twice when the job is retried?

Build Idempotency-Key from the record, as above, not from the time. If the future method runs again for the same Opportunity with the same body, Sume answers 200 with the original run and idempotency_hit: true: no second run, no second charge. The same key with a different body is 409 idempotency_conflict, and nothing runs. Bump the version suffix when you want a new video on purpose.

Salesforce also recommends Queueable Apex over future methods in general: it adds job ids, non-primitive member variables and job chaining.

How do I get the finished video back?

Not from the callout. Store data.id from the response on the record, then let Sume POST the terminal receipt to communication.webhook_url, a public HTTPS endpoint you run, or poll GET /v1/format-runs/{run_id} with backoff until the status is terminal. Signed webhooks for Sume video runs covers verifying the POST, and the run's primary_output_url points at the video once it completes.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume