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
Forgot / Reset Password10 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)
403Forbidden — affiliate is not in a loginable status (currently approved or active). Returned by POST /clicks when the resolved affiliate's status is unapproved, rejected, or deactivated.
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": "Invalid credentials"
}

Endpoints using the flat shape: POST /auth/login, POST /auth/forgot-password, POST /auth/reset-password, POST /applications, POST /affiliates/{affiliateSlug}/clicks, GET /affiliates/{affiliateSlug}/clicks, GET /affiliates/{affiliateSlug}/clicks/total, GET /affiliates/{affiliateSlug}/revenue/total, GET /affiliates/{affiliateSlug}/commission/expected, GET /affiliates/{affiliateSlug}/payouts/history, POST /affiliates/{affiliateSlug}/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/{affiliateSlug}/bank-account, GET /affiliates/{affiliateSlug}/notifications, POST /affiliates/{affiliateSlug}/notifications/mark-all-read, POST /affiliates/{affiliateSlug}/notifications/{notificationId}/read, GET /affiliates/{affiliateSlug}/performance/sub-ids.

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

Affiliate identifier in URLs is a slug, not a UUID. All /affiliates/{affiliateSlug}/... endpoints expect the affiliate's slug — a URL-safe, stable identifier returned by POST /auth/login in the slug field. The UUID id field in the login response is internal and is not used in URLs. Unknown slugs return 404 Affiliate not found.

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",
    "slug": "demo-affiliate",
    "name": "Demo Affiliate",
    "email": "affiliate@example.com",
    "commissionPercentage": 10,
    "referralLink": "https://bluecarrental.is?ref=demo-affiliate",
    "bankAccount": {
      "holderName": "Demo Affiliate ehf.",
      "bankName": "Landsbankinn",
      "iban": "IS140159260076545510730339",
      "swift": "NBIIISRE"
    }
  }
}

The slug field is the URL-safe identifier you include in every per-affiliate endpoint path (e.g. /affiliates/{slug}/clicks). The id UUID is internal — informational only; do not use it as the URL key.

The commissionPercentage field is the affiliate's commission rate as a percentage (e.g. 10 means 10%). It is a number, or null when the rate has not yet been configured — distinct from 0, which would mean "zero commission". Store it client-side after login and use it to render the rate in your UI or compute per-month owed when reconciling revenue. The value only refreshes on next login; if BlueDesk changes the rate mid-session, the partner sees the old value until the affiliate signs in again.

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",
    "slug": "demo-affiliate",
    "name": "Demo Affiliate",
    "email": "affiliate@example.com",
    "commissionPercentage": null,
    "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.

Request Password Reset

POST /api/v1/partner/auth/forgot-password

Start the password reset flow for an affiliate. BlueDesk emails a single-use reset link to the affiliate's registered address.

Request Body

{
  "email": "affiliate@example.com"
}

Response

200 Success — always returned for any well-formed request

{
  "message": "If that email is registered, a reset link has been sent."
}

The endpoint is enumeration-safe: the same 200 response is returned whether the email matches an affiliate or not, and whether the underlying email delivery succeeded or failed. Your portal should always show a neutral confirmation screen after calling this endpoint.

400 Validation error (flat error shape)

{
  "error": "Email is required"
}

Returned only when the request body is malformed or the email field is missing / not an email address.

Behaviour:
  • Only affiliates with status approved or active receive a reset email. Other statuses (unapproved, rejected, deactivated) still get the generic 200 with no side effects.
  • The link in the email expires in 1 hour and can be used at most once.
  • Requesting a new reset before the previous one is consumed automatically invalidates the previous link.
  • Rate limit: 10 requests per minute per IP.

Complete Password Reset

POST /api/v1/partner/auth/reset-password

Consume a reset token and set a new affiliate password. The token is the value embedded in the link sent by POST /auth/forgot-password.

Request Body

{
  "token": "the-token-from-the-reset-link",
  "password": "the-new-password"
}

Parameters

FieldRequiredDescription
tokenYesThe reset token from the email link. Single-use; consumed on first successful call.
passwordYesNew password. Minimum 8 characters.

Response

200 Success

{
  "message": "Password reset successfully"
}

400 Validation or token error (flat error shape)

Missing fields
{
  "error": "Token and password are required"
}
Password too short
{
  "error": "Password must be at least 8 characters"
}
Invalid, expired, or already-consumed token
{
  "error": "Reset link is invalid or has expired"
}

Token failures all return the same generic message regardless of whether the token never existed, has expired, or has already been used. This prevents an attacker from probing the API to learn which tokens are valid.

500 Failed to reset password (flat error shape)

{
  "error": "Failed to reset password"
}

Returned when the new password hash cannot be generated or when consuming the reset token fails for a reason other than the token being absent. Safe to retry with backoff.

Behaviour:
  • Successfully consuming a token immediately invalidates it; subsequent calls with the same token return the generic 400.
  • The affiliate's password hash and updated_at are both replaced atomically.
  • Rate limit: 10 requests per minute per IP.

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",
  "name": "Demo Affiliate",
  "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)
nameYesAffiliate's full name; doubles as the Caren join key for reservation attribution.
companyNameNoCompany or property name
websiteNoWebsite URL
instagramNoInstagram handle
tiktokNoTikTok handle
youtubeNoYouTube channel
facebookNoFacebook page

The name field is the single attribution key used to join clicks and revenue back to the affiliate via Caren. It must be non-empty after trimming.

Response

201 Created

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "slug": "nordic-travel-co",
  "email": "new-affiliate@example.com",
  "name": "Demo Affiliate",
  "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": "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/{affiliateSlug}/clicks

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

Path Parameters

ParameterDescription
affiliateSlugThe affiliate's slug (from the slug field in the login response). URL-safe identifier; use this in the URL path. The earlier affiliateId placeholder name was renamed to remove the UUID implication.

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"
}

403 Affiliate is not active (flat error shape)

{
  "error": "Affiliate is not active"
}

Returned when the affiliate's status is anything other than approved or active (for example unapproved, rejected, or deactivated). Clicks for paused affiliates are not recorded.

404 Affiliate not found (flat error shape)

{
  "error": "Affiliate not found"
}

Returned when the {affiliateSlug} path segment does not match any affiliate's slug. This is the response you will see if you accidentally template the UUID id or the affiliate's name into the URL instead of the slug.

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/{affiliateSlug}/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"
}

404 Affiliate not found (flat error shape) — the {affiliateSlug} path segment did not match any affiliate.

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/{affiliateSlug}/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)

404 Affiliate not found (flat error shape) — the {affiliateSlug} path segment did not match any affiliate.

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/{affiliateSlug}/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"
  }
}

404 Affiliate not found (nested error shape) — the {affiliateSlug} path segment did not match any affiliate.

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/{affiliateSlug}/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)

404 Affiliate not found (flat error shape) — the {affiliateSlug} path segment did not match any affiliate.

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/{affiliateSlug}/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)

404 Affiliate not found (flat error shape) — the {affiliateSlug} path segment did not match any affiliate.

500 Server error (flat error shape)

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

Server error — safe to retry with backoff.

Payout History

GET /api/v1/partner/affiliates/{affiliateSlug}/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": 19800,
      "status": "paid",
      "requestDate": "2026-05-05",
      "paidDate": "2026-05-10",
      "months": [
        {
          "monthId": "2026-05",
          "amount": 8000,
          "reservations": [
            { "reservationCode": "9LTE18", "reservationId": "res-guid-1", "amount": 5000, "carenReservationId": 2065791, "verification": "verified" },
            { "reservationCode": "9LTE22", "reservationId": "res-guid-2", "amount": 3000, "carenReservationId": 2065844, "verification": "flagged", "verificationReason": "amount_mismatch", "expectedAmount": 2450 }
          ]
        },
        { "monthId": "2026-04", "amount": 11800 }
      ],
      "createdAt": "2026-05-05T10:00:00Z",
      "updatedAt": "2026-05-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,
      "months": [],
      "createdAt": "2026-03-15T09:30:00Z",
      "updatedAt": "2026-03-15T09:30:00Z"
    }
  ]
}

months is always present in the response. Legacy payouts (submitted before the per-month breakdown shipped, or submitted via the scalar-amount body shape) return "months": []. Within a month, reservations is only echoed back when it was submitted — months submitted without a reservation itemization return no reservations key. createdAt is the ISO-8601 UTC timestamp when the row was inserted — use it to render "requested at" in your UI. Each reservation entry may also carry carenReservationId (numeric, read-only) — a field populated by Blue after the payout is accepted once the background enrichment process completes. Payouts queried immediately after creation may show reservations without this field; the field appears once enrichment finishes and can be observed in subsequent GET /payouts/history calls.

Each reservation entry may also carry verification, verificationReason, and expectedAmount — read-only fields populated by Blue's background verification process on the same asynchronous schedule as carenReservationId. verification is one of verified, flagged, or unverifiable, and is absent until Blue's verification for that reservation completes. verificationReason (one of amount_mismatch, not_found, rate_not_configured) is present only when verification is flagged or unverifiable. expectedAmount (the commission Blue computed from Caren booking data) is present only when verificationReason is amount_mismatch. All three fields are output-only; Blue verifies each reservation's claimed amount against Caren booking data independently of anything the partner submits.

Payout Status Values

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

404 Affiliate not found (flat error shape) — the {affiliateSlug} path segment did not match any affiliate.

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/{affiliateSlug}/payouts/request

Submit a payout request. Requests are accepted subject to payload validation below (no balance check is performed — the partner computes the affiliate's balance on their own side) and then verified: Blue's background process checks each reservation's claimed amount against Caren booking data, and a Blue admin approves the request before payment. Two request-body shapes are accepted — the per-month breakdown is preferred; the legacy scalar-amount shape is kept for older integrations.

Request Body — per-month breakdown (preferred)

{
  "months": [
    {
      "monthId": "2026-05",
      "amount": 8000,
      "reservations": [
        { "reservationCode": "9LTE18", "reservationId": "res-guid-1", "amount": 5000 },
        { "reservationCode": "9LTE22", "reservationId": "res-guid-2", "amount": 3000 }
      ]
    },
    { "monthId": "2026-04", "amount": 11800 }
  ],
  "total": 19800
}

reservations is optional per month. A month can be submitted with only monthId and amount (as with the second month above), or itemized down to individual reservations. Mixing itemized and non-itemized months in the same request is allowed.

Request Body — legacy scalar amount

{
  "amount": 500
}

Request Fields

FieldTypeRequiredDescription
monthsarraypreferredPer-month breakdown of the payout. When present, total is required and amount is ignored. Persisted verbatim on the payout row for reporting.
months[].monthIdstringyes (if months present)ISO year-month identifier matching ^\d{4}-(0[1-9]|1[0-2])$ — for example 2026-05. 2026-5 and May-2026 are rejected.
months[].amountnumberyes (if months present)Must be strictly greater than zero.
months[].reservationsarrayoptionalPer-reservation itemization of the month. When present, the reservation amounts must sum to the month's amount within the same 0.01 tolerance. Months without a reservations key remain valid.
months[].reservations[].reservationCodestringyes (if reservations present)Caren reservation code (e.g. 9LTE18). Non-empty, at most 50 characters.
months[].reservations[].reservationIdstringyes (if reservations present)Caren reservation GUID, stored opaque. Non-empty, at most 100 characters. Must be unique within the request. A reservationId already claimed in a previous non-rejected payout is accepted but flagged for admin review.
months[].reservations[].amountnumberyes (if reservations present)Strictly greater than zero.
months[].reservations[].carenReservationIdnumberno (output only)Numeric Caren reservation ID, populated by Blue after the payout is accepted. Read-only on output; any value sent in the request is ignored. Present only after Blue's background enrichment resolves the reservation against Caren.
months[].reservations[].verificationstringno (output only)One of verified, flagged, unverifiable. Read-only; populated by Blue's background verification process once it completes for that reservation. Absent until then. Any value sent in the request is ignored.
months[].reservations[].verificationReasonstringno (output only)One of amount_mismatch, not_found, rate_not_configured. Read-only; present only when verification is flagged or unverifiable. Any value sent in the request is ignored.
months[].reservations[].expectedAmountnumberno (output only)The commission Blue computed from Caren booking data for this reservation. Read-only; present only when verificationReason is amount_mismatch. Any value sent in the request is ignored.
totalnumberyes (if months present)Strictly greater than zero. Must equal the sum of months[].amount within a tolerance of 0.01. Used as an integrity check on the submitted breakdown — the stored amount is the server-computed sum of the month amounts, so a partner cannot rely on a mismatched total to alter it.
amountnumberonly when months is absentLegacy scalar amount. Strictly greater than zero.

Validation rules

Response

201 Created

{
  "id": "c3d4e5f6-a7b8-9012-cdef-ab3456789012",
  "affiliateId": "3f8a92b1-4e5c-4d2a-9f1b-8c7d6e5f4a3b",
  "amount": 19800,
  "status": "pending",
  "requestDate": "2026-05-31",
  "paidDate": null,
  "months": [
    {
      "monthId": "2026-05",
      "amount": 8000,
      "reservations": [
        { "reservationCode": "9LTE18", "reservationId": "res-guid-1", "amount": 5000 },
        { "reservationCode": "9LTE22", "reservationId": "res-guid-2", "amount": 3000 }
      ]
    },
    { "monthId": "2026-04", "amount": 11800 }
  ],
  "createdAt": "2026-05-31T12:00:00Z",
  "updatedAt": "2026-05-31T12:00:00Z"
}

Legacy scalar-amount requests receive the same shape with "months": [].

Within each reservation entry, carenReservationId is read-only and populated by Blue after the payout is accepted. The creation response never includes it — enrichment runs asynchronously after the response is sent, so the field only appears on subsequent GET /payouts/history calls once Blue's background process resolves the reservation against Caren. Partners must not send carenReservationId in the request body; any value is silently ignored.

400 Validation error (flat error shape). The error field is one of:

{
  "error": "total does not match sum of months"
}

404 Affiliate not found (flat error shape) — the {affiliateSlug} path segment did not match any affiliate.

500 Server error (flat error shape)

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

Server error — safe to retry with backoff.

The months breakdown is stored for reporting only — the admin approves and pays the scalar amount, which equals the server-side sum of the month amounts.
Verification never rejects a request at submission time. After creation, Blue's background process checks each itemized reservation's claimed amount against Caren booking data, populating verification (and, when applicable, verificationReason / expectedAmount) asynchronously — see the carenReservationId and verification field notes under Payout History. A flagged or unverifiable reservation does not block the payout; approval by a Blue admin remains the gate before payment.

Get Bank Account

GET /api/v1/partner/affiliates/{affiliateSlug}/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 slug itself does not exist.

Update Bank Account

PUT /api/v1/partner/affiliates/{affiliateSlug}/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/{affiliateSlug}/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.

404 Affiliate not found (nested error shape) — the {affiliateSlug} slug did not match any affiliate.

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/{affiliateSlug}/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.

404 Affiliate not found (nested error shape) — the {affiliateSlug} slug did not match any affiliate.

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/{affiliateSlug}/notifications/{notificationId}/read

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

Path Parameters

ParameterDescription
affiliateSlugThe affiliate's slug (from the login response). URL-safe identifier.
notificationIdThe notification's UUID (from the list endpoint). Notification IDs are UUIDs, not slugs.

Response

200 Success

{
  "status": "ok"
}

400 Validation error (nested error shape)

The message identifies the offending field. Possible values:

MessageCause
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"
  }
}

404 Affiliate not found (nested error shape) — the {affiliateSlug} path segment did not match any affiliate.

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 IDAffiliate slug. URL-safe, stable, returned by POST /auth/login in the slug field. Include in all subsequent requests. The id UUID in the login response is internal and is not used in URLs.
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