Java SDK
FynxtCrmClient is a small Java client for the
CRM Onboarding APIs. Use it from your CRM backend to
register CRM users, create their IB, Affiliate and Trader profiles, and attach their
trading accounts so the FYNXT Incentive Program can attribute trading activity and pay
out rebates and rewards. Once your CRM has paid a settlement, use it to
report the payout outcome back to FYNXT.
Install
The SDK is a single source file with no FYNXT runtime. Copy
FynxtCrmClient.java into your codebase and move it to a
package of your choice. It needs Java 17+ (it uses
java.net.http.HttpClient and records) and Jackson for JSON:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.17.2</version>
</dependency>
Spring Boot projects already include both. There is no Maven artifact to download.
Authentication
Every call carries your tenant's CRM API key in the X-Tenant-Api-Key header.
The key identifies your brokerage on its own. The client adds the header for you.
X-Tenant-Api-Key: {{tenantCrmApiKey}}
Contact FYNXT to have a CRM API key issued for your brokerage. Keep it on the server: it must never reach a browser or mobile app.
Initialize the client
Build one client and reuse it. It is thread-safe and holds a single
HttpClient connection pool.
FynxtCrmClient fynxt = FynxtCrmClient.builder()
.baseUrl("https://incentive-program-app.fynxt.com")
.tenantApiKey(System.getenv("FYNXT_CRM_API_KEY"))
.actor("1", "CRM", "crm@yourbroker.com", "Broker")
.build();
X-Tenant-Api-Key.actionDoneById, actionDoneByName,
actionDoneByEmail and actionDoneByRole. The client fills these
into every request that does not set its own. Set a request's own actor when a CRM
operator, not the system, made the change.
Onboarding flow
Onboarding a client into the Incentive Program takes three steps, in this order:
-
Create the CRM user. A CRM user (CRM account) is the person or company
in your CRM, keyed by your
crmAccountId. It holds one or more profiles. -
Create their profiles. Each profile is an
IB,AffiliateorTraderrole for that user, keyed by yourexternalId. One CRM user can hold several, for example an IB who also trades. Steps 1 and 2 are one call:upsertAccountProfiles. -
Attach trading accounts. Link each trading-server login to the
Trader profile that owns it and the IB or Affiliate who referred it, with
attachTradingAccount. From then on, that login's trades count toward the referrer's programs.
referredById and a
trading account's externalReferredById must name a profile FYNXT already
knows. Upsert the IB or Affiliate before the clients they referred.
Create a CRM user & profiles
upsertAccountProfiles(request) creates the CRM user if
crmAccountId is new and creates or updates each profile in
profiles. Sending the same crmAccountId again updates the
existing user. Calls POST /api/abstraction/crm/upsert-account-profiles and
returns the CrmAccount.
CrmAccount account = fynxt.upsertAccountProfiles(
new UpsertAccountProfilesRequest()
.crmAccountId("CRM-USER-00001")
.crmTenantId("54322")
.accountStatus(Status.Active)
.addProfile(new Profile()
.type(ProfileType.IB)
.name("John Smith")
.country("United Kingdom")
.contactEmail("john.smith@ib-partner.io")
.accountCurrency("USD")
.status(Status.Active)
.address("10 Downing St, London")
.dateOfBirth(LocalDate.of(1985, 6, 15))
.kycStatus("Verified")
.referredById("EXT-MIB-001") // parent IB; omit for a top-level IB
.externalId("EXT-IB-001")
.referralLinkId(3L)
.isDeleted(false)
.onboardingStatus(OnboardingStatus.Completed)));
Request fields
"CRM-USER-09876"). The upsert key for the CRM user.Active or Inactive.Profile types
| Type | Who | Referred by |
|---|---|---|
IB | Introducing Broker. Refers clients and earns rebates on their trading. | Optional. Set referredById to a parent IB's externalId to build a multi-level IB tree. |
Affiliate | Marketing partner. Earns on referred client activity, usually CPA. | Optional. Same as IB. |
Trader | End client who trades. Owns trading accounts. | The IB or Affiliate who referred them, if any. |
FYNXT rejects a referredById chain that loops back on itself.
Profile fields
IB, Affiliate or Trader."EXT-TRADER-006"). The upsert key for the profile, and the id other profiles and trading accounts use to refer to it.Active or Inactive."EUR")."Canada").YYYY-MM-DD."Verified" or "Pending".externalId of the IB or Affiliate who referred this profile.NotStarted, InProgress, Completed or Rejected.true to soft-delete the profile.Updating profiles
updateAccountProfiles(request) takes the same request and calls
PUT /api/abstraction/crm/upsert-account-profiles. Use it when a profile
changes in your CRM: name, email, status, KYC, onboarding stage or referrer.
On an update, referredById works in three ways:
| You send | Result |
|---|---|
not set (null) | The current referrer is kept. |
"" (empty string) | The profile becomes top level, with no referrer. |
an externalId | The profile moves under that referrer. |
The client never sends fields you left unset, so leaving referredById alone
keeps the referrer.
Attaching trading accounts
attachTradingAccount(request) links a login on a trading server to a Trader
profile and its referrer. Calls
POST /api/abstraction/crm/crm/trading-accounts and returns the
TradingAccount.
TradingAccount ta = fynxt.attachTradingAccount(
new AttachTradingAccountRequest()
.tradeServerId("tsv_01knh7qz80e61tv085bqwf06y1")
.tradeLoginId("110001")
.externalClientId("EXT-TRADER-006")
.externalReferredById("EXT-IB-001")
.currency("USD")
.isDeleted(false)
.status(TradingAccountStatus.Active));
- no account with
tradeLoginIdexists ontradeServerIdyet, externalClientIdis not a Trader profile,externalReferredByIdis missing or is not an IB or Affiliate,- the account is already mapped to another client or another referrer. Moving an account between referrers is a client movement, done in the FYNXT back office.
Request fields
"tsv_01knh7qz80e61tv085bqwf06y1")."110001".externalId of the Trader profile that owns the account.externalId of the IB or Affiliate the account is attributed to."USD").Active, Inactive or Blocked.externalReferredById, be Active, and
promote a live rebate program.
false.Error handling
Every method throws FynxtApiException (unchecked) on a non-2xx response or a
network failure. statusCode() is the HTTP status, or 0 when no
response arrived. body() is the raw response body, which may be empty.
| Status | Meaning | What to do |
|---|---|---|
400 | The request broke a rule: unknown profile or referrer, wrong profile type, trading account not found, referral link not usable, circular referrer chain. | Fix the data. Do not retry unchanged. |
401 / 403 | Missing or wrong API key, or a crmTenantId from another tenant. | Check the key and tenant. |
5xx / 0 | Server or network failure. | Retry with backoff. Both calls are upserts, so retrying is safe. |
In production the API does not return the reason for a 400. Log the request
you sent along with the status.
Client source
Copy this file as is. Replace the package name with your own.
package com.yourbroker.fynxt;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** Client for the FYNXT Incentive CRM APIs: onboarding, settlement and event tracking. Thread-safe; build once and reuse. */
public final class FynxtCrmClient {
private static final String UPSERT_PROFILES = "/api/abstraction/crm/upsert-account-profiles";
private static final String TRADING_ACCOUNTS = "/api/abstraction/crm/crm/trading-accounts";
private static final String REBATE_SETTLEMENT_UPDATE = "/api/abstraction/crm/api/settlements/settlement-update";
private static final String CPA_SETTLEMENT_UPDATE = "/api/abstraction/crm/cpa-scheme/settlement/settlement-update";
private static final String REWARD_SETTLEMENT_UPDATE = "/api/abstraction/crm/api/settlements/reward/settlement-update";
private static final String SETTLE_REBATES = "/api/abstraction/crm/api/settlements/settle/";
private static final String SETTLE_CPA = "/api/abstraction/crm/cpa-scheme/settlement/settle/";
private static final String SETTLE_REWARDS = "/api/abstraction/crm/api/settlements/reward/settle/";
private static final String TRACK_EVENT = "/api/referral/events/track";
private final String baseUrl;
private final String[] authHeaders;
private final Actor defaultActor;
private final Duration timeout;
private final HttpClient http;
private final ObjectMapper json = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
private FynxtCrmClient(Builder b) {
this.baseUrl = Objects.requireNonNull(b.baseUrl, "baseUrl").replaceAll("/+$", "");
this.authHeaders = Objects.requireNonNull(b.authHeaders, "tenantApiKey or legacyApplicationKey");
this.defaultActor = b.actor;
this.timeout = b.timeout;
this.http = HttpClient.newBuilder().connectTimeout(b.timeout).build();
}
public static Builder builder() { return new Builder(); }
/** Creates or updates a CRM user and the given profiles (POST). */
public CrmAccount upsertAccountProfiles(UpsertAccountProfilesRequest request) {
return send("POST", UPSERT_PROFILES, request, CrmAccount.class);
}
/** Edits an existing CRM user's profiles (PUT). A null referredById keeps the current referrer. */
public CrmAccount updateAccountProfiles(UpsertAccountProfilesRequest request) {
return send("PUT", UPSERT_PROFILES, request, CrmAccount.class);
}
/** Maps an existing trading-server login to a Trader profile and its IB or Affiliate. */
public TradingAccount attachTradingAccount(AttachTradingAccountRequest request) {
return send("POST", TRADING_ACCOUNTS, request, TradingAccount.class);
}
/** Records a referral conversion event (deposit, KYC, first trade, ...) from your backend. */
public TrackEventResult trackEvent(TrackEventRequest request) {
return send("POST", TRACK_EVENT, request, TrackEventResult.class);
}
/**
* Settles a rebate program's unsettled rewards: one record per IB, published to Kafka and,
* for legacy systems, to the settlement webhook. Returns the records created.
*/
public SettlementRun settleRebates(long programId) {
return send("POST", SETTLE_REBATES + programId, null, SettlementRun.class);
}
/** Settles a CPA program's unsettled rewards and publishes them to Kafka. Returns the records created. */
public SettlementRun settleCpa(long programId) {
return send("POST", SETTLE_CPA + programId, null, SettlementRun.class);
}
/** Settles a reward program's unsettled rewards and publishes them to Kafka. data() is a status message. */
public SettlementResult settleRewards(long programId) {
return send("POST", SETTLE_REWARDS + programId, null, SettlementResult.class);
}
/** Reports the payout outcome of rebate settlement records. */
public SettlementResult updateRebateSettlements(SettlementUpdate update) {
return send("POST", REBATE_SETTLEMENT_UPDATE, update, SettlementResult.class);
}
/** Reports the payout outcome of CPA settlement records. externalRefId must be the numeric record id. */
public SettlementResult updateCpaSettlements(SettlementUpdate update) {
return send("POST", CPA_SETTLEMENT_UPDATE, update, SettlementResult.class);
}
/** Reports the payout outcome of reward settlement records. */
public SettlementResult updateRewardSettlements(RewardSettlementUpdate update) {
return send("POST", REWARD_SETTLEMENT_UPDATE, update, SettlementResult.class);
}
private <T> T send(String method, String path, Object body, Class<T> type) {
if (body instanceof AuditedRequest<?> r && r.actionDoneById == null && defaultActor != null) r.actor(defaultActor);
try {
HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + path))
.timeout(timeout)
.header("Content-Type", "application/json")
.headers(authHeaders)
.method(method, body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(json.writeValueAsString(body)))
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() / 100 != 2) {
throw new FynxtApiException(response.statusCode(), response.body(), null);
}
return response.body().isBlank() ? null : json.readValue(response.body(), type);
} catch (IOException | InterruptedException e) {
if (e instanceof InterruptedException) Thread.currentThread().interrupt();
throw new FynxtApiException(0, null, e);
}
}
public static final class Builder {
private String baseUrl;
private String[] authHeaders;
private Actor actor;
private Duration timeout = Duration.ofSeconds(30);
public Builder baseUrl(String v) { baseUrl = v; return this; }
/** Your tenant's CRM API key, sent as X-Tenant-Api-Key. */
public Builder tenantApiKey(String key) { authHeaders = new String[] { "X-Tenant-Api-Key", key }; return this; }
/** Legacy shared application key: X-API-KEY plus X-Tenant-Slug. */
public Builder legacyApplicationKey(String key, String tenantSlug) {
authHeaders = new String[] { "X-API-KEY", key, "X-Tenant-Slug", tenantSlug };
return this;
}
/** Default actor for requests that do not set their own. */
public Builder actor(String id, String name, String email, String role) { actor = new Actor(id, name, email, role); return this; }
public Builder timeout(Duration v) { timeout = v; return this; }
public FynxtCrmClient build() { return new FynxtCrmClient(this); }
}
// --- Requests -------------------------------------------------------------
public record Actor(String id, String name, String email, String role) {}
/** Who made the change, recorded in the FYNXT audit trail. */
abstract static class AuditedRequest<T extends AuditedRequest<T>> {
private String actionDoneById, actionDoneByName, actionDoneByEmail, actionDoneByRole;
public T actor(String id, String name, String email, String role) { return actor(new Actor(id, name, email, role)); }
@SuppressWarnings("unchecked")
T actor(Actor a) {
actionDoneById = a.id(); actionDoneByName = a.name(); actionDoneByEmail = a.email(); actionDoneByRole = a.role();
return (T) this;
}
}
public static final class UpsertAccountProfilesRequest extends AuditedRequest<UpsertAccountProfilesRequest> {
private String crmAccountId;
private String crmTenantId;
private Status accountStatus;
private final List<Profile> profiles = new ArrayList<>();
public UpsertAccountProfilesRequest crmAccountId(String v) { crmAccountId = v; return this; }
/** The FYNXT tenant id assigned to your brokerage. */
public UpsertAccountProfilesRequest accountStatus(Status v) { accountStatus = v; return this; }
public UpsertAccountProfilesRequest crmTenantId(Integer v) { crmTenantId = v; return this; }
public UpsertAccountProfilesRequest addProfile(Profile p) { profiles.add(p); return this; }
}
public static final class Profile {
private ProfileType type;
private String externalId, name, contactEmail, accountCurrency, country, address, kycStatus, referredById;
private Status status;
private LocalDate dateOfBirth;
private Long referralLinkId;
private OnboardingStatus onboardingStatus;
private Boolean isDeleted;
public Profile type(ProfileType v) { type = v; return this; }
public Profile externalId(String v) { externalId = v; return this; }
public Profile name(String v) { name = v; return this; }
public Profile contactEmail(String v) { contactEmail = v; return this; }
public Profile status(Status v) { status = v; return this; }
public Profile accountCurrency(String v) { accountCurrency = v; return this; }
public Profile country(String v) { country = v; return this; }
public Profile address(String v) { address = v; return this; }
public Profile dateOfBirth(LocalDate v) { dateOfBirth = v; return this; }
public Profile kycStatus(String v) { kycStatus = v; return this; }
/** On an update: null keeps the referrer, "" makes the profile top level. */
public Profile referredById(String v) { referredById = v; return this; }
public Profile referralLinkId(Long v) { referralLinkId = v; return this; }
public Profile onboardingStatus(OnboardingStatus v) { onboardingStatus = v; return this; }
public Profile isDeleted(Boolean v) { isDeleted = v; return this; }
}
public static final class AttachTradingAccountRequest extends AuditedRequest<AttachTradingAccountRequest> {
private String tradeServerId, tradeLoginId, externalClientId, externalReferredById, currency;
private TradingAccountStatus status;
private Long referralLinkId;
private Boolean isDeleted;
public AttachTradingAccountRequest tradeServerId(String v) { tradeServerId = v; return this; }
public AttachTradingAccountRequest tradeLoginId(String v) { tradeLoginId = v; return this; }
public AttachTradingAccountRequest externalClientId(String v) { externalClientId = v; return this; }
public AttachTradingAccountRequest externalReferredById(String v) { externalReferredById = v; return this; }
public AttachTradingAccountRequest currency(String v) { currency = v; return this; }
public AttachTradingAccountRequest status(TradingAccountStatus v) { status = v; return this; }
public AttachTradingAccountRequest referralLinkId(Long v) { referralLinkId = v; return this; }
public AttachTradingAccountRequest isDeleted(Boolean v) { isDeleted = v; return this; }
}
/** Rebate or CPA payout outcomes. batchReferenceId is your CRM's batch id, if it batches. */
public record SettlementUpdate(String batchReferenceId, List<Payout> payouts) {}
/** externalRefId: the settlementRecordId (or, for rebate only, the settlementEventId) FYNXT sent. */
public record Payout(String externalRefId, String crmReferenceId, PayoutStatus status,
String failureReason, Instant processedAt) {
public static Payout success(String externalRefId, String crmReferenceId) {
return new Payout(externalRefId, crmReferenceId, PayoutStatus.SUCCESS, null, Instant.now());
}
public static Payout failed(String externalRefId, String crmReferenceId, String failureReason) {
return new Payout(externalRefId, crmReferenceId, PayoutStatus.FAILED, failureReason, Instant.now());
}
}
public record RewardSettlementUpdate(String batchReferenceId, List<RewardPayout> payouts) {}
/** Matched by settlementRecordId; settlementEventId is used only when settlementRecordId is null. */
public record RewardPayout(Long settlementRecordId, String settlementEventId, String crmReferenceId,
PayoutStatus status, String failureReason, Instant processedAt) {
public static RewardPayout success(Long settlementRecordId, String crmReferenceId) {
return new RewardPayout(settlementRecordId, null, crmReferenceId, PayoutStatus.SUCCESS, null, Instant.now());
}
public static RewardPayout failed(Long settlementRecordId, String crmReferenceId, String failureReason) {
return new RewardPayout(settlementRecordId, null, crmReferenceId, PayoutStatus.FAILED, failureReason, Instant.now());
}
}
public static final class TrackEventRequest {
private EventType event;
private Long brokerId, clientId;
private String clickId, currency, metadata;
private BigDecimal amount;
private final Map<String, Object> data = new LinkedHashMap<>();
public TrackEventRequest event(EventType v) { event = v; return this; }
/** Your FYNXT tenant id. Must match the tenant of your API key. */
public TrackEventRequest brokerId(Long v) { brokerId = v; return this; }
/** The click-id from the referral link the client arrived through. */
public TrackEventRequest clickId(String v) { clickId = v; return this; }
/** FYNXT's internal id of the client's Trader profile. Takes precedence over email. */
public TrackEventRequest clientId(Long v) { clientId = v; return this; }
/** Finds the Trader profile with this contact email, or creates one. */
public TrackEventRequest email(String v) { data.put("email", v); return this; }
/** Name for a Trader profile created from email. */
public TrackEventRequest name(String v) { data.put("name", v); return this; }
public TrackEventRequest amount(BigDecimal v) { amount = v; return this; }
public TrackEventRequest currency(String v) { currency = v; return this; }
/** Free-form JSON object, stored with the event as is. */
public TrackEventRequest metadata(String json) { metadata = json; return this; }
}
// Constant names are the exact values the API accepts.
public enum EventType {
REFERRAL_INITIATED, EMAIL_SIGNUP, ACCOUNT_CREATION, REGISTRATION, VERIFICATION, LOGIN,
FIRST_DEPOSIT, RECURRING_DEPOSIT, FIRST_TRADE, RECURRING_TRADE, KYC_COMPLETED, WITHDRAWAL,
ACCOUNT_UPGRADE, SUBSCRIPTION, CUSTOM, CONTENT_DOWNLOAD, WEBINAR_REGISTRATION, NEWSLETTER_SUBSCRIPTION
}
public enum PayoutStatus { SUCCESS, FAILED }
public enum ProfileType { IB, Affiliate, Trader }
public enum Status { Active, Inactive }
public enum OnboardingStatus { NotStarted, InProgress, Completed, Rejected }
public enum TradingAccountStatus { Active, Inactive, Blocked }
// --- Responses ------------------------------------------------------------
public record CrmAccount(Long id, String crmAccountId, String status, String country,
String timezone, String language, String registrationDate) {}
public record TradingAccount(Long id, Long profileId, String tradeServerId, String platformLogin,
String currency, String type, String status, String name) {}
/** FYNXT's response envelope. Unknown or already-decided records are skipped, not errors. */
public record SettlementResult(Object data, ResultStatus status) {}
public record TrackEventResult(TrackedEvent data, ResultStatus status) {}
/** duplicate: this client (or click) already had an event of this type, so it does not count again. */
public record TrackedEvent(Long id, String eventType, Long clientId, String timestamp, BigDecimal amount,
String currency, boolean conversion, boolean duplicate, Long clickId) {}
/** Records created by a rebate or CPA settle call; empty when nothing was unsettled. */
public record SettlementRun(List<SettlementRecord> data, ResultStatus status) {}
/** settlementId is the record id to echo back as externalRefId in a settlement update. */
public record SettlementRecord(String settlementId, String settlementRunId, String crmRefer, String settlementStatus,
String ibId, String tierId, String amount, String currency) {}
public record ResultStatus(int code, String message) {}
public static final class FynxtApiException extends RuntimeException {
private final int statusCode;
private final String body;
FynxtApiException(int statusCode, String body, Throwable cause) {
super(statusCode == 0 ? "FYNXT request failed" : "FYNXT API returned HTTP " + statusCode, cause);
this.statusCode = statusCode;
this.body = body;
}
/** HTTP status, or 0 when no response arrived. */
public int statusCode() { return statusCode; }
public String body() { return body; }
}
}