RefCampaign

Error handling

Error response shape, common codes, and retry strategy.

Every error response returned by the API follows the same shape. Codes are stable and machine-readable; messages are human-readable and may change wording between releases.

Response shape

{
  "success": false,
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Validation failed.",
    "details": [
      {
        "path": ["amount"],
        "message": "Number must be greater than 0"
      }
    ]
  },
  "meta": {
    "timestamp": "2026-05-03T10:23:11.000Z"
  }
}
FieldTypeNotes
successfalseAlways false on errors.
error.codestringStable enum — switch on this in your client code.
error.messagestringHuman-readable. Suitable for logs, not for end-users.
error.detailsunknownFree-form context. For VALIDATION_FAILED, an array of Zod issues.
meta.timestampstringISO-8601 UTC. Useful for support tickets.

The HTTP status code mirrors the broad failure category (4xx for client errors, 5xx for server errors). The error.code further narrows it down.

Common codes

HTTPerror.codeWhen you'll see it
400VALIDATION_FAILEDRequest body or query params don't match the schema. details lists the field-level issues.
400INVALID_REQUESTBody is unparsable JSON, or the request shape is fundamentally wrong (missing required content-type, etc.).
401UNAUTHORIZEDMissing, malformed, or revoked API key.
403PERMISSION_DENIEDAuthenticated but not allowed (wrong account, locked merchant, expired subscription).
404NOT_FOUNDThe resource id doesn't exist or doesn't belong to your account.
409ALREADY_EXISTSDuplicate creation (e.g. an affiliate with the same email already exists in this campaign).
429RATE_LIMIT_EXCEEDEDPer-IP or per-account rate limit hit. Honour Retry-After.
429LIMIT_EXCEEDEDSubscription-tier quota hit (e.g. monthly conversion cap). Upgrade or wait for reset.
500SERVER_ERRORGeneric upstream failure. Retry with backoff.
503SERVICE_UNAVAILABLEA downstream service (payment provider, queue) is degraded. Retry.

The full list ships in the OpenAPI spec under components.schemas.ApiErrorResponse.properties.error.code.enum.

Retry strategy

A robust client distinguishes three cases:

  1. Terminal client error (4xx except 429) — your payload is wrong. Do NOT retry. Log, fix the call site.
  2. Transient server error (5xx) — RefCampaign upstream is unhappy. Retry with exponential backoff (1s, 2s, 4s, 8s) up to ~5 attempts.
  3. Rate limited (429) — back off according to the Retry-After header. The server tells you when it's safe to come back.

Pseudocode:

async function callWithRetry(fetcher, maxAttempts = 5) {
  let attempt = 0
  while (attempt < maxAttempts) {
    const res = await fetcher()
    if (res.ok) return res
    if (res.status === 429) {
      const retryAfter = parseInt(res.headers.get('retry-after') ?? '5', 10)
      await sleep(retryAfter * 1000)
      attempt++
      continue
    }
    if (res.status >= 500) {
      await sleep(Math.min(2 ** attempt * 1000, 30_000))
      attempt++
      continue
    }
    throw await errorFromResponse(res) // 4xx — terminal
  }
  throw new Error('Exhausted retries')
}

The official @refcampaign/sdk handles all of this for you.

Validation errors

VALIDATION_FAILED returns error.details as an array of Zod issues:

{
  "success": false,
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Validation failed.",
    "details": [
      { "path": ["currency"], "code": "invalid_string", "message": "currency must be a 3-letter ISO code" },
      { "path": ["amount"], "code": "too_small", "message": "Number must be greater than 0" }
    ]
  }
}

The path array is the dotted path into your request body or query params. Surface these in your UI / logs to point users at the specific field.

Idempotency and duplicate-error handling

Some endpoints are idempotent on a key (POST /api/v1/conversions/track on sessionId within five minutes — see postback). If your retry logic fires a duplicate, you'll get the same successful response, not a 409. 409 ALREADY_EXISTS is reserved for genuine duplicate creation attempts (e.g. signing up the same email to the same campaign twice).

When to alert

Consider treating these as paging-worthy:

  • Any sustained 5xx rate above ~1% over 5 min — RefCampaign side issue.
  • Any 429 RATE_LIMIT_EXCEEDED you didn't expect — your traffic shape changed; review.
  • 403 PERMISSION_DENIED outside of expected role-restricted paths — your subscription may have changed status.

401 UNAUTHORIZED after a deploy almost always means the API key wasn't propagated to the new environment.

On this page