Skip to content

Building API integrations

An integration translates what your application wants into an external API call. Give each operation a command or query of its own, so callers can say “set the target temperature” without knowing the URL, headers or JSON format.

This teaching API accepts POST https://heating.example/target with { "celsius": 21.0 } and returns HTTP 204. Its token comes from heating.api.token (HEATING_API_TOKEN in the environment).

public record SetHeatingTarget(double celsius) {
@HandleCommand
void handle() {
var request = WebRequest.post("https://heating.example/target")
.header("Authorization", "Bearer "
+ ApplicationProperties.requireProperty("heating.api.token"))
.contentType("application/json")
.body(new TargetTemperature(celsius))
.build();
var response = Fluxzero.sendWebRequestAndWait(request);
if (response.getStatus() != 204) {
throw new IllegalStateException(
"Heating API returned HTTP " + response.getStatus());
}
}
}
public record TargetTemperature(double celsius) {}

From an active application context, send SetHeatingTarget(21.0) through Fluxzero.sendCommandAndWait(...). The self-handler runs locally without registration. Its HTTP request still uses the ordinary Fluxzero web gateway, including tracing, audit and configured transport behavior. Do not invoke handle() directly.

An external read follows the same pattern with @HandleQuery and Request<YourReply>. The web-request guide shows response decoding and transport settings.

Run the actual command handler. A fixture HTTP handler supplies the remote response; no API-service mock or external account is needed:

@Consumer(name = "heating-api-stub")
class HeatingApiStub {
@HandlePost("https://heating.example/target")
WebResponse setTarget(TargetTemperature target) {
return WebResponse.builder().status(204).build();
}
}
@Test
void sendsTheRequestedTarget() {
TestFixture.create(new HeatingApiStub())
.withProperty("heating.api.token", "test-token")
.whenCommand(new SetHeatingTarget(21.0))
.expectSuccessfulResult()
.expectOnlyWebRequests(WebRequest.post("https://heating.example/target")
.header("Authorization", "Bearer test-token")
.contentType("application/json")
.body(new TargetTemperature(21.0)).build());
}

The stub’s named consumer gives it independent processing when the fixture runs asynchronously. The assertion checks the method, destination, credential header and payload. Change the stub’s response to test rejected credentials or remote failures. Use TestFixture.createAsync(...) as well when the surrounding workflow uses tracked handlers or retries.

Grow the integration without hiding the operations

Section titled “Grow the integration without hiding the operations”

Once several operations share authentication and request settings, extract those conventions into a small helper, interface or base class. Keep the concrete command/query responsible for its operation and result type. For example, SetHeatingTarget and GetHeatingStatus can share header construction without becoming one generic “call URL” command.

Keep credentials in ApplicationProperties. If operators configure different installations, let application state select a trusted configuration group; do not accept arbitrary URLs or tokens in public domain commands. Provider request types belong with the integration, while the domain’s API stays independent of that provider.

The first example propagates an unexpected HTTP status as a technical failure. As the API grows, distinguish known refusals, temporary unavailability and malformed responses. Return or throw outcomes your workflow can act on, without copying remote bodies or credentials into user-facing errors. Unexpected programming failures should still surface as failures.

Choose timeouts and transport retries using WebRequestSettings. A retry must be appropriate for that operation: “set target to 21” can usually be repeated more safely than “increase target by 1”. A timeout can mean the remote operation succeeded but its response was lost.

Connect external work to committed application state

Section titled “Connect external work to committed application state”

If the API call follows a domain change, first commit that change. A tracked event handler can then send the integration command. Keep external I/O out of @Apply, because applies can run again during replay or conflict recovery. The HTTP call is not part of the Model transaction.

flowchart LR
    C[Domain command] --> M[Commit application intent]
    M --> H[Tracked handler]
    H --> O[Integration command]
    O --> A[External API]

A local integration command is one invocation, not a durable job by itself. For work that must survive interruption, retain pending intent in a stateful workflow and let its processing/retry policy drive the operation. Do not add @TrackSelf merely because the command sends HTTP.

An acknowledgement is not the final observation

Section titled “An acknowledgement is not the final observation”

HTTP 204 says the heating API accepted the target. It does not prove that the room has reached 21 °C. Read observations separately and keep their meaning distinct from the requested setting. If a person later changes the physical controls, decide whether your product should follow that change or actively enforce a target. Do not accidentally restore an old, already completed request.

For an external action that creates something, retain an operation ID before sending it and reuse the same ID when retrying. When the provider supports an idempotency key, map that retained ID to its request header. Generating a new key for each attempt creates new operations. Respect the provider’s retention window and use an authoritative lookup when the outcome is uncertain.

Test interruption after the external call as well as an explicit HTTP error. A successful response followed by a failed local acknowledgement is a different recovery case. The workflow decides when work is complete; the gateway’s transport retry cannot decide that business outcome.

Follow the pattern in the example applications

Section titled “Follow the pattern in the example applications”

The public examples show these refinements with concrete product rules. These links point to the revisions reviewed for this guide:


© 2026 Fluxzero