Skip to content
API referenceAuthentication and errors

API reference

API reference

One HTTPS API with JSON in and out. Your backend signs requests to the App API with an app key; partners call the partner API with a Bearer key. The pages below list every endpoint.

Base URL

https://api.flowgroup.se

The same host serves the sandbox and production: the key you use decides the environment. The full API is also described as an OpenAPI 3.1 document at /v1/openapi.json, generated from the same schemas the API validates with.

Signed requests

Every App API request is signed with an app key: a key id (fg_test_… or fg_live_…) and a secret, issued per app and environment.

x-fg-keyheaderrequired
The key id.
x-fg-timestampheaderrequired
Unix time in seconds, within 300 seconds of our clock.
x-fg-signatureheaderrequired

Lower-case hex HMAC-SHA256(secret, "<timestamp>.<raw body>"). A GET request signs an empty body.

const signature = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');

Sign the exact bytes you send, and sign again for every retry. Events has full examples in curl and Node; the Node SDK signs for you. Reward webhooks are signed the same way in the other direction, see Webhooks.

Partner API keys

Partners create keys on the Developers page of their portal and send them as a Bearer token:

Authorization: Bearer fa_live_…

Keys start with fa_live_ (production data) or fa_test_ (sandbox data: test conversions and commissions that are never paid). Each key has scopes, and an endpoint answers 403 insufficient_scope when the key lacks the one it needs:

links:readscope
List and read links with their click and conversion counts.
links:writescope
Create links in programs you are an active member of.
conversions:readscope
List conversions.
reports:readscope
Commissions, the payout summary and reports.

A revoked, expired or unknown key answers 401 invalid_api_key, and the keys of a suspended account answer 403 partner_suspended.

Errors

Every error has the same shape:

{
  "error": {
    "code": "code_inactive",
    "message": "code_inactive"
  }
}

Branch on code, which is stable; message is for people and may change. A 400 validation_error carries details that mirror your request: properties.<field>.errors lists what is wrong with each field.

400Bad Request
validation_error, or a malformed body. Fix the request before sending it again.
401Unauthorized
The signature or key was refused. The code says why.
403Forbidden
The key may not do this (insufficient_scope, partner_suspended, membership_required).
404Not Found
The thing named does not exist for your key, for example not_found for an unknown code.
409Conflict
The request is valid but the state does not allow it (already_attributed, production_only).
429Too Many Requests
Slow down and retry after Retry-After seconds.
5xxServer error
Retry with backoff. Event ids make retrying events safe.

Rate limits

The default limits:

App API1,200 per minute per key

On /v1/events, /v1/attributions, /v1/affiliates/* and /v1/referrals/*. Answers 429 too_many_attempts. Batch events, up to 500 per request.

Partner API120 per minute per key

Answers 429 rate_limited. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.

Pagination

Partner API lists are cursor-paginated, newest first. Pass limit (1–100, default 50) and, for the next page, the cursor from the previous response:

{ "items": [], "hasMore": true, "nextCursor": "MjAyNi0xMS0wOVQxMjozMTowNy40MTJa…" }

A cursor is opaque: pass it back unchanged. When hasMore is false, nextCursor is null and you have everything.

Versioning

The API is versioned in the path (/v1). Within a version, changes are additive: new endpoints, new optional parameters, new fields in responses and new event and webhook types. Ignore fields you don't know, and treat enums as open. A change that could break a working integration gets a new version.