Blue's Affiliate API

This API enables partner systems to integrate with BlueDesk's affiliate management platform. It provides endpoints for affiliate authentication, click tracking, revenue reporting, payout management, bank-account configuration, and in-portal notifications.

The API is designed for server-to-server communication. Your backend calls these endpoints on behalf of affiliates — the affiliate portal should never call BlueDesk directly.

Authentication

All requests must include an API key in the X-API-Key header. API keys are issued by BlueDesk administrators.

Header
X-API-Key: bdp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Requests without a valid API key will receive a 401 Unauthorized response.

Base URL

https://affiliate-api.bluedesk.is
Staging environment: https://affiliate-api-staging.bluedesk.is

Rate Limits

ScopeLimitWindow
Global (all endpoints)1,000 requestsPer minute
Login10 requestsPer minute per IP
Click ingestion500 requestsPer minute per API key

Exceeded limits return 429 Too Many Requests.

Note: rate-limit responses are plain text, not JSON. The body is Rate limit exceeded and the Content-Type is text/plain; charset=utf-8. This differs from all other partner API endpoints — clients should branch on Content-Type (or status code 429) rather than always JSON-decoding the body.

Error Handling

The API uses standard HTTP status codes:

CodeMeaning
200Success
201Created
400Bad request (validation error)
401Unauthorized (missing/invalid API key or credentials)
404Resource not found
409Conflict (e.g., duplicate email)
429Rate limit exceeded
500Server error

Error Response Shapes

Two error response shapes currently coexist while the API completes a transition. Both are JSON, both carry a single human-readable message, and both will be returned with the appropriate HTTP status code. Partners must handle both shapes; a future flag-day migration will unify them.

Flat (legacy) — used by endpoints that were live before the affiliate portal launched:

{
  "error": "Amount exceeds available balance"
}

Endpoints using the flat shape: POST /auth/login, POST /applications, POST /affiliates/{id}/clicks, GET /affiliates/{id}/clicks, GET /affiliates/{id}/clicks/total, GET /affiliates/{id}/revenue/total, GET /affiliates/{id}/commission/expected, GET /affiliates/{id}/payouts/balance, GET /affiliates/{id}/payouts/history, POST /affiliates/{id}/payouts/request.

Nested (new) — used by every endpoint added 2026-04 onwards:

{
  "error": {
    "message": "invalid IBAN format"
  }
}

Endpoints using the nested shape: GET and PUT /affiliates/{id}/bank-account, GET /affiliates/{id}/notifications, POST /affiliates/{id}/notifications/mark-all-read, POST /affiliates/{id}/notifications/{notificationId}/read, GET /affiliates/{id}/performance/sub-ids.

Each endpoint section below shows the correct shape for that endpoint's error responses.

Authenticate Affiliate

POST /api/v1/partner/auth/login

Validate affiliate credentials. Returns affiliate identity for your system to issue its own session token.

Request Body

{
  "email": "affiliate@example.com",
  "password": "their-password"
}

Response

200 Success

{
  "affiliate": {
    "id": "3f8a92b1-4e5c-4d2a-9f1b-8c7d6e5f4a3b",
    "name": "Demo Affiliate",
    "email": "affiliate@example.com",
    "referralLink": "https://bluecarrental.is?ref=demo-affiliate",
    "bankAccount": {
      "holderName": "Demo Affiliate ehf.",
      "bankName": "Landsbankinn",
      "iban": "IS140159260076545510730339",
      "swift": "NBIIISRE"
    }
  }
}

The bankAccount field is always present in the response. It is null when the affiliate has not yet configured a bank account, allowing your system to render a "configure your account" CTA without an extra round-trip.

When no bank account is configured
{
  "affiliate": {
    "id": "3f8a92b1-4e5c-4d2a-9f1b-8c7d6e5f4a3b",
    "name": "Demo Affiliate",
    "email": "affiliate@example.com",
    "referralLink": "https://bluecarrental.is?ref=demo-affiliate",
    "bankAccount": null
  }
}

400 Validation error (flat error shape)

{
  "error": "Email and password are required"
}

"Invalid request body" is also returned with 400 for malformed JSON.

401 Invalid credentials (flat error shape)

{
  "error": "Invalid credentials"
}
Only affiliates with status approved or active can log in. Unapproved, rejected, or deactivated affiliates receive 401 with the same generic message to avoid leaking account state.

Submit Affiliate Application

POST /api/v1/partner/applications

Submit a new affiliate application from the "Join Us" form. Creates an affiliate with status unapproved.

Request Body

{
  "email": "new-affiliate@example.com",
  "password": "securepassword123",
  "firstName": "John",
  "lastName": "Smith",
  "companyName": "Nordic Travel Co",
  "website": "https://nordictravel.co",
  "instagram": "@nordictravel",
  "tiktok": "",
  "youtube": "",
  "facebook": "nordictravel"
}

Parameters

FieldRequiredDescription
emailYesAffiliate's email address (unique)
passwordYesPassword (min 8 characters)
firstNameYesFirst name
lastNameYesLast name
companyNameNoCompany or property name
websiteNoWebsite URL
instagramNoInstagram handle
tiktokNoTikTok handle
youtubeNoYouTube channel
facebookNoFacebook page

Response

201 Created

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "slug": "nordic-travel-co",
  "email": "new-affiliate@example.com",
  "firstName": "John",
  "lastName": "Smith",
  "companyName": "Nordic Travel Co",
  "status": "unapproved",
  "availableBalance": 0,
  "carenSetupComplete": false,
  "createdAt": "2026-03-31T12:00:00Z",
  "updatedAt": "2026-03-31T12:00:00Z"
}

The full Affiliate record is returned. Optional fields (e.g. companyName, website, social handles) are omitted when empty; availableBalance, carenSetupComplete, status, createdAt, and updatedAt are always present.

400 Validation error (flat error shape)

{
  "error": "First name, last name, email, and password are required"
}

"Invalid request body" is also returned with 400 for malformed JSON.

409 Email already registered (flat error shape)

{
  "error": "An account with this email already exists"
}

500 Server error (flat error shape)

{
  "error": "Failed to process application"
}

Server error — safe to retry with backoff.

Record Click Event

POST /api/v1/partner/affiliates/{affiliateId}/clicks

Record a referral link click. Call this whenever a visitor arrives via an affiliate's referral link.

Path Parameters

ParameterDescription
affiliateIdThe affiliate's UUID (from login response)

Request Body

All fields are optional and backward-compatible. Old clients that send only country and sourceApp continue to work unchanged.

{
  "country": "IS",
  "sourceApp": "website",
  "subId": "promo-spring",
  "source": "instagram",
  "medium": "social",
  "campaign": "spring-2026"
}

Parameters

FieldRequiredDescription
countryNoVisitor's ISO country code
sourceAppNoIdentifier for the referring platform (e.g., website, app)
subIdNoPartner's own sub-affiliate identifier — surfaces in the Sub-ID Performance endpoint
sourceNoUTM source. Stored as utm_source
mediumNoUTM medium. Stored as utm_medium
campaignNoUTM campaign. Stored as utm_campaign
Note the JSON keys for UTM fields are source, medium, and campaign — not utmSource / utmMedium / utmCampaign.

Response

201 Created

{
  "status": "ok"
}

400 Invalid affiliate ID format (flat error shape)

{
  "error": "Invalid affiliate ID format"
}

404 Affiliate not found (flat error shape)

{
  "error": "Affiliate not found"
}

500 Failed to record click (flat error shape)

{
  "error": "Failed to record click"
}

Server error — safe to retry with backoff.

Daily Click Breakdown

GET /api/v1/partner/affiliates/{affiliateId}/clicks

Get daily click counts for a date range. Every day in the range is included, even if the count is zero.

Query Parameters

ParameterRequiredDescription
fromYesStart date in YYYY-MM-DD format (inclusive)
toYesEnd date in YYYY-MM-DD format (inclusive). Must not be before from.

Response

200 Success

{
  "clicksPerDay": [
    { "date": "2026-03-01", "value": 94 },
    { "date": "2026-03-02", "value": 87 },
    { "date": "2026-03-03", "value": 0 }
  ]
}

Each item's date is a calendar day in YYYY-MM-DD form — not an ISO timestamp — and value is the integer click count for that day.

400 Missing or invalid date range (flat error shape)

{
  "error": "from and to query parameters are required"
}

500 Server error (flat error shape)

{
  "error": "Failed to get daily clicks"
}

Server error — safe to retry with backoff.

Total Clicks

GET /api/v1/partner/affiliates/{affiliateId}/clicks/total

Get the total click count for a date range.

Query Parameters

ParameterRequiredDescription
fromYesStart date in YYYY-MM-DD format (inclusive)
toYesEnd date in YYYY-MM-DD format (inclusive). Must not be before from.

Response

200 Success

{
  "total": 1234
}

400 Missing or invalid date range (flat error shape)

500 Server error (flat error shape)

{
  "error": "Failed to get total clicks"
}

Server error — safe to retry with backoff.

Sub-ID Performance

GET /api/v1/partner/affiliates/{affiliateId}/performance/sub-ids

Per-sub-id click breakdown for the affiliate's referral traffic. Use this to see how individual sub-affiliate identifiers and UTM combinations are performing.

Query Parameters

ParameterRequiredDescription
fromNoStart date in YYYY-MM-DD format (inclusive)
toNoEnd date in YYYY-MM-DD format (inclusive). Must not be before from.

If both from and to are omitted, the endpoint defaults to the last 30 days (to = today UTC, from = today − 30 days). Supplying only one of the two still returns 400 — partial date ranges are not allowed, since one-sided defaults would silently widen a caller's query window.

Response

200 Success

[
  {
    "subId": "promo-spring",
    "source": "instagram",
    "medium": "social",
    "campaign": "spring-2026",
    "clicks": 142,
    "bookings": null,
    "conversionPercent": null,
    "revenue": null
  }
]

Rows are ordered by clicks DESC, then subId ASC for stable rendering. Clicks with a NULL sub_id are excluded. The source, medium, and campaign fields are taken from the most recent click's UTM tags for each sub-id.

The bookings, conversionPercent, and revenue fields are intentionally null in this release. Booking attribution is tracked separately and will be wired into this endpoint in a follow-up — until then, treat them as not-yet-available rather than zero.

400 Invalid date range (nested error shape)

{
  "error": {
    "message": "from and to query parameters are required"
  }
}

500 Server error (nested error shape)

{
  "error": {
    "message": "Failed to get sub-id performance"
  }
}

Server error — safe to retry with backoff.

Revenue Total

GET /api/v1/partner/affiliates/{affiliateId}/revenue/total

Get total revenue generated through the affiliate's referral link for a date range.

Query Parameters

ParameterRequiredDescription
fromYesStart date in YYYY-MM-DD format (inclusive)
toYesEnd date in YYYY-MM-DD format (inclusive). Must not be before from.

Response

200 Success

{
  "total": 45000
}
Returns 0 if no revenue data has been recorded for the period.

400 Missing or invalid date range (flat error shape)

500 Server error (flat error shape)

{
  "error": "Failed to get revenue"
}

Server error — safe to retry with backoff.

Expected Commission

GET /api/v1/partner/affiliates/{affiliateId}/commission/expected

Get expected commission for a date range.

Query Parameters

ParameterRequiredDescription
fromYesStart date in YYYY-MM-DD format (inclusive)
toYesEnd date in YYYY-MM-DD format (inclusive). Must not be before from.

Response

200 Success

{
  "total": 2250
}
Returns 0 if no commission data has been recorded for the period.

400 Missing or invalid date range (flat error shape)

500 Server error (flat error shape)

{
  "error": "Failed to get commission"
}

Server error — safe to retry with backoff.

Payout Balance

GET /api/v1/partner/affiliates/{affiliateId}/payouts/balance

Get the affiliate's current available payout balance.

Response

200 Success

{
  "availableBalance": 2845
}

500 Server error (flat error shape)

{
  "error": "Failed to get balance"
}

Server error — safe to retry with backoff.

Payout History

GET /api/v1/partner/affiliates/{affiliateId}/payouts/history

Get the full payout transaction history for an affiliate.

Response

200 Success

{
  "history": [
    {
      "id": "f1e2d3c4-b5a6-7890-fedc-ba0987654321",
      "affiliateId": "3f8a92b1-4e5c-4d2a-9f1b-8c7d6e5f4a3b",
      "amount": 1200,
      "status": "paid",
      "requestDate": "2026-01-05",
      "paidDate": "2026-01-10",
      "createdAt": "2026-01-05T10:00:00Z",
      "updatedAt": "2026-01-10T14:00:00Z"
    },
    {
      "id": "a9b8c7d6-e5f4-3210-abcd-ef9876543210",
      "affiliateId": "3f8a92b1-4e5c-4d2a-9f1b-8c7d6e5f4a3b",
      "amount": 500,
      "status": "pending",
      "requestDate": "2026-03-15",
      "paidDate": null,
      "createdAt": "2026-03-15T09:30:00Z",
      "updatedAt": "2026-03-15T09:30:00Z"
    }
  ]
}

Payout Status Values

StatusDescription
pendingRequest submitted, awaiting approval
approvedApproved by administrator, awaiting payment
paidPayment completed
rejectedRequest rejected by administrator

500 Server error (flat error shape)

{
  "error": "Failed to get payout history"
}

Server error — safe to retry with backoff.

Submit Payout Request

POST /api/v1/partner/affiliates/{affiliateId}/payouts/request

Submit a payout request. The amount is validated against the affiliate's available balance.

Request Body

{
  "amount": 500
}

Response

201 Created

{
  "id": "c3d4e5f6-a7b8-9012-cdef-ab3456789012",
  "affiliateId": "3f8a92b1-4e5c-4d2a-9f1b-8c7d6e5f4a3b",
  "amount": 500,
  "status": "pending",
  "requestDate": "2026-03-31",
  "paidDate": null,
  "createdAt": "2026-03-31T12:00:00Z",
  "updatedAt": "2026-03-31T12:00:00Z"
}

400 Validation error (flat error shape)

{
  "error": "Amount exceeds available balance"
}

500 Server error (flat error shape)

{
  "error": "Failed to create payout request"
}

Server error — safe to retry with backoff.

The balance is deducted atomically when the request is created. If the request is later rejected, the balance is restored by an administrator.

Get Bank Account

GET /api/v1/partner/affiliates/{affiliateId}/bank-account

Get the affiliate's configured bank account. Returns the same shape that appears under affiliate.bankAccount in the login response, without the wrapper.

Response

200 Success

{
  "holderName": "Demo Affiliate ehf.",
  "bankName": "Landsbankinn",
  "iban": "IS140159260076545510730339",
  "swift": "NBIIISRE"
}

The swift field is null when no SWIFT/BIC code has been recorded.

404 Bank account not configured (nested error shape)

{
  "error": {
    "message": "Bank account not configured"
  }
}

404 Affiliate not found (nested error shape)

{
  "error": {
    "message": "Affiliate not found"
  }
}

500 Server error (nested error shape)

{
  "error": {
    "message": "Failed to get bank account"
  }
}

Server error — safe to retry with backoff.

404 with the message Bank account not configured is the explicit "needs configuration" signal — the portal uses it to render the configure-account CTA. A separate 404 with Affiliate not found is returned when the affiliate UUID itself does not exist.

Update Bank Account

PUT /api/v1/partner/affiliates/{affiliateId}/bank-account

Create or replace the affiliate's bank-account details. The IBAN is persisted in canonical form (uppercased, internal spaces stripped).

Request Body

{
  "holderName": "Demo Affiliate ehf.",
  "bankName": "Landsbankinn",
  "iban": "IS14 0159 2600 7654 5510 7303 39",
  "swift": "NBIIISRE"
}

Parameters

FieldRequiredDescription
holderNameYesAccount holder name. Non-empty after trimming.
bankNameYesBank name. Non-empty after trimming.
ibanYesIBAN or Iceland domestic account number. See accepted formats below.
swiftNoSWIFT/BIC code. May be omitted, null, or empty — all three normalise to NULL in storage.

Accepted IBAN Formats

The iban field accepts any of the following (whitespace and case are normalised before matching):

FormatExampleDescription
Iceland domestic 4-2-60159-26-007654Four-digit bank code, two-digit branch, six-digit account. Dashes optional.
Iceland IBANIS140159260076545510730339IS followed by 24 digits (26 chars total).
Generic ISO 13616 IBANDE89370400440532013000Two-letter country code, two check digits, then 11–30 alphanumerics. Total length 15–34 chars.
Validation is structural only — checksum verification is delegated to the receiving bank at payout time. Banks frequently print IBANs with grouped spaces (e.g. IS14 0159…); these are accepted and stored canonical (no spaces, uppercase).

Response

200 Success — returns the saved bank account in canonical form.

{
  "holderName": "Demo Affiliate ehf.",
  "bankName": "Landsbankinn",
  "iban": "IS140159260076545510730339",
  "swift": "NBIIISRE"
}

400 Validation error (nested error shape)

The message identifies the offending field. Possible values:

MessageCause
holderName is requiredholderName missing or whitespace-only
bankName is requiredbankName missing or whitespace-only
iban is requirediban missing or whitespace-only
invalid IBAN formatiban does not match any accepted format
Invalid request bodyJSON parse error
{
  "error": {
    "message": "invalid IBAN format"
  }
}

404 Affiliate not found (nested error shape)

{
  "error": {
    "message": "Affiliate not found"
  }
}

500 Server error (nested error shape)

{
  "error": {
    "message": "Failed to update bank account"
  }
}

Server error — safe to retry with backoff.

List Notifications

GET /api/v1/partner/affiliates/{affiliateId}/notifications

Get the affiliate's most recent notifications, ordered by creation time descending. Returns up to 100 records.

Response

200 Success — always an array, never null.

[
  {
    "id": "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
    "title": "Payout approved",
    "body": "Your payout request of 500 ISK has been approved.",
    "createdAt": "2026-04-30T12:00:00Z",
    "read": false
  }
]

The read field is a real boolean — it becomes true once the notification's read_at timestamp is stamped. createdAt is RFC3339 UTC.

400 Invalid affiliate ID format (nested error shape)

{
  "error": {
    "message": "invalid affiliate ID format"
  }
}

404 Affiliate not found (nested error shape)

500 Server error (nested error shape)

{
  "error": {
    "message": "Failed to list notifications"
  }
}

Server error — safe to retry with backoff.

Mark All Notifications Read

POST /api/v1/partner/affiliates/{affiliateId}/notifications/mark-all-read

Stamp read_at on every unread notification for the affiliate. No request body required.

Response

200 Success — returns the count of rows updated.

{
  "updated": 3
}

updated is a non-negative integer (int64).

Idempotent: a follow-up call when no unread notifications remain returns {"updated": 0} with the same 200 status.

400 Invalid affiliate ID format (nested error shape)

{
  "error": {
    "message": "invalid affiliate ID format"
  }
}

404 Affiliate not found (nested error shape)

500 Server error (nested error shape)

{
  "error": {
    "message": "Failed to mark notifications read"
  }
}

Server error — safe to retry with backoff.

Mark Notification Read

POST /api/v1/partner/affiliates/{affiliateId}/notifications/{notificationId}/read

Stamp read_at on a single notification. No request body required.

Path Parameters

ParameterDescription
affiliateIdThe affiliate's UUID
notificationIdThe notification's UUID (from the list endpoint)

Response

200 Success

{
  "status": "ok"
}

400 Validation error (nested error shape)

The message identifies the offending field. Possible values:

MessageCause
invalid affiliate ID formataffiliateId is not a valid UUID
notification ID is requirednotificationId path segment missing or empty
invalid notification ID formatnotificationId is not a valid UUID
{
  "error": {
    "message": "invalid notification ID format"
  }
}

404 Notification not found (nested error shape)

{
  "error": {
    "message": "Notification not found"
  }
}

500 Server error (nested error shape)

{
  "error": {
    "message": "Failed to mark notification read"
  }
}

Server error — safe to retry with backoff.

Notifications are scoped to the affiliate. The 404 is returned both when the notification UUID does not exist and when it belongs to a different affiliate — the two cases are intentionally indistinguishable to avoid leaking which notification IDs exist on other accounts.

General Notes

TopicDetail
Date formatQuery parameters (from, to) are always YYYY-MM-DD. Response timestamps are RFC3339 UTC (YYYY-MM-DDTHH:MM:SSZ). The date field in clicksPerDay is a calendar day (YYYY-MM-DD), not a timestamp.
CurrencyAll monetary amounts are in ISK (Icelandic Króna). Monetary fields are JSON numbers (not strings). Whole-króna amounts serialize without a decimal point (e.g. 2845, not 2845.00); fractional amounts include a decimal point as needed.
Affiliate IDUUID returned from the login endpoint. Include in all subsequent requests.
Content-TypeAll requests and responses use application/json
Empty arraysList endpoints — including notifications and sub-id performance — return [] (not null) when no records exist
Error shapesTwo shapes coexist: flat ({"error": "..."}) for endpoints predating the affiliate portal, and nested ({"error": {"message": "..."}}) for endpoints added 2026-04 onwards. See the Error Handling section above.

© 2026 Blue Car Rental — Blue's Affiliate API

Questions? Contact gudmundur@bluecarrental.is