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.

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:
| Situation | What Salesforce requires |
|---|---|
| Callout from a trigger or after DML | A future method or the Queueable interface |
| Any external endpoint | Authorized in Remote Site Settings, or reached through a named credential |
| Future method arguments | Primitive types or collections of them, such as a list of record ids |
| Order of future methods | Not guaranteed; two can run at the same time |
| Apex tests | No 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
- Trailhead: Make callouts to external services (read 2026-09-29)
- Trailhead: Apex REST callouts (read 2026-09-29)
- Trailhead: Use future methods (read 2026-09-29)
- Trailhead: Protect secrets using platform features (read 2026-09-29)
- Salesforce Apex Recipes: RestClient.cls (read 2026-09-29)
- Create a run
- Runs and results
- Authentication
Related posts
More in Integrations
- Spring Retry and @Retryable for a paid API call in Spring 7
Spring Retry is archived; Spring Framework 7 has @Retryable in core. For a paid API call, narrow the exceptions, back off, and pass one idempotency key in.
- Stripe checkout webhook: start a paid job once per payment
Listen for checkout.session.completed, verify the signature on the raw body, answer 2xx fast, then start the job keyed by the Checkout Session id.
- Tenacity retry in Python: safe settings for a paid API POST
Bare @retry in tenacity retries forever with no wait. For a paid POST, cap attempts, add jittered backoff, retry only transient errors, and reuse one key.
- Workato HTTP connector: call an API with a key and JSON
Workato's HTTP connector calls any HTTP API: a Header auth connection holds the key, and Send request via HTTP POSTs a JSON body and maps the reply.
Written by Sume