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.
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
https://affiliate-api-staging.bluedesk.isRate Limits
| Scope | Limit | Window |
|---|---|---|
| Global (all endpoints) | 1,000 requests | Per minute |
| Login | 10 requests | Per minute per IP |
| Click ingestion | 500 requests | Per minute per API key |
Exceeded limits return 429 Too Many Requests.
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:
| Code | Meaning |
|---|---|
200 | Success |
201 | Created |
400 | Bad request (validation error) |
401 | Unauthorized (missing/invalid API key or credentials) |
404 | Resource not found |
409 | Conflict (e.g., duplicate email) |
429 | Rate limit exceeded |
500 | Server 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
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.
{
"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"
}
Submit Affiliate Application
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
| Field | Required | Description |
|---|---|---|
email | Yes | Affiliate's email address (unique) |
password | Yes | Password (min 8 characters) |
firstName | Yes | First name |
lastName | Yes | Last name |
companyName | No | Company or property name |
website | No | Website URL |
instagram | No | Instagram handle |
tiktok | No | TikTok handle |
youtube | No | YouTube channel |
facebook | No | Facebook 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
Record a referral link click. Call this whenever a visitor arrives via an affiliate's referral link.
Path Parameters
| Parameter | Description |
|---|---|
affiliateId | The 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
| Field | Required | Description |
|---|---|---|
country | No | Visitor's ISO country code |
sourceApp | No | Identifier for the referring platform (e.g., website, app) |
subId | No | Partner's own sub-affiliate identifier — surfaces in the Sub-ID Performance endpoint |
source | No | UTM source. Stored as utm_source |
medium | No | UTM medium. Stored as utm_medium |
campaign | No | UTM campaign. Stored as utm_campaign |
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 daily click counts for a date range. Every day in the range is included, even if the count is zero.
Query Parameters
| Parameter | Required | Description |
|---|---|---|
from | Yes | Start date in YYYY-MM-DD format (inclusive) |
to | Yes | End 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 the total click count for a date range.
Query Parameters
| Parameter | Required | Description |
|---|---|---|
from | Yes | Start date in YYYY-MM-DD format (inclusive) |
to | Yes | End 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
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
| Parameter | Required | Description |
|---|---|---|
from | No | Start date in YYYY-MM-DD format (inclusive) |
to | No | End 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.
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 total revenue generated through the affiliate's referral link for a date range.
Query Parameters
| Parameter | Required | Description |
|---|---|---|
from | Yes | Start date in YYYY-MM-DD format (inclusive) |
to | Yes | End date in YYYY-MM-DD format (inclusive). Must not be before from. |
Response
200 Success
{
"total": 45000
}
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 expected commission for a date range.
Query Parameters
| Parameter | Required | Description |
|---|---|---|
from | Yes | Start date in YYYY-MM-DD format (inclusive) |
to | Yes | End date in YYYY-MM-DD format (inclusive). Must not be before from. |
Response
200 Success
{
"total": 2250
}
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 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 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
| Status | Description |
|---|---|
pending | Request submitted, awaiting approval |
approved | Approved by administrator, awaiting payment |
paid | Payment completed |
rejected | Request 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
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.
Get 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.
Update 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
| Field | Required | Description |
|---|---|---|
holderName | Yes | Account holder name. Non-empty after trimming. |
bankName | Yes | Bank name. Non-empty after trimming. |
iban | Yes | IBAN or Iceland domestic account number. See accepted formats below. |
swift | No | SWIFT/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):
| Format | Example | Description |
|---|---|---|
| Iceland domestic 4-2-6 | 0159-26-007654 | Four-digit bank code, two-digit branch, six-digit account. Dashes optional. |
| Iceland IBAN | IS140159260076545510730339 | IS followed by 24 digits (26 chars total). |
| Generic ISO 13616 IBAN | DE89370400440532013000 | Two-letter country code, two check digits, then 11–30 alphanumerics. Total length 15–34 chars. |
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:
| Message | Cause |
|---|---|
holderName is required | holderName missing or whitespace-only |
bankName is required | bankName missing or whitespace-only |
iban is required | iban missing or whitespace-only |
invalid IBAN format | iban does not match any accepted format |
Invalid request body | JSON 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 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
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).
{"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
Stamp read_at on a single notification. No request body required.
Path Parameters
| Parameter | Description |
|---|---|
affiliateId | The affiliate's UUID |
notificationId | The 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:
| Message | Cause |
|---|---|
invalid affiliate ID format | affiliateId is not a valid UUID |
notification ID is required | notificationId path segment missing or empty |
invalid notification ID format | notificationId 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.
General Notes
| Topic | Detail |
|---|---|
| Date format | Query 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. |
| Currency | All 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 ID | UUID returned from the login endpoint. Include in all subsequent requests. |
| Content-Type | All requests and responses use application/json |
| Empty arrays | List endpoints — including notifications and sub-id performance — return [] (not null) when no records exist |
| Error shapes | Two 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