HeldSway

Referral Links API

Create, manage, track, and bulk-generate branded referral links with coupon attachments, QR codes, and custom campaign tracking parameters.


Authentication & Scopes

All endpoints require a Bearer access token obtained via POST /v1/auth/token.

Scope Access
affiliates:read List links, view link stats, and poll link creation batches
affiliates:write Create links (single and bulk), update status, attach coupons, and delete links


List all referral links across all affiliates for your business.

GET /v1/links

Required scope: affiliates:read Rate limit: 120 requests/minute

Query Parameters

Parameter Type Default Description
page integer 1 Page number
per_page integer 25 Results per page. Max 100
type string product product, affiliate or all. Only product links are returned unless you pass type=affiliate or type=all
affiliate_id integer — Filter to a specific affiliate
is_active string — 0, 1, true or false. Only 1 selects active links; 0, true and false all select inactive links (ReferralLinkService::list() compares with '1')
search string — Search slug, destination URL, campaign, product name, or affiliate name/email (max 255)
location_id integer — Filter by location ID (the rule requires an integer)
product_name string — Case-insensitive partial match on the product name (max 255)
sort string created_at Sort field: created_at | slug | click_count | conversion_count | destination_url | campaign. Any other value leaves the order unspecified
direction string desc asc | desc

Rules from ListReferralLinksApiRequest::rules(); filtering in ReferralLinkService::list().

Example Request

curl "https://api.heldsway.com/api/v1/links?is_active=1&per_page=20" \
  -H "Authorization: Bearer <access_token>"

Success Response — 200 OK

{
  "success": true,
  "message": "OK",
  "data": {
    "items": [
      {
        "id": 10,
        "affiliate_id": 42,
        "affiliate_user": { "name": "Jane Smith", "email": "jane@example.com" },
        "slug": "summer2026",
        "destination_url": "https://example.com/landing",
        "campaign": "Summer Campaign",
        "discount_code": null,
        "is_active": true,
        "click_count": 142,
        "conversion_count": 9,
        "tracking_url": "https://yourbusiness.heldsway.com/r/summer2026",
        "qr_code_url": "https://yourbusiness.heldsway.com/r/summer2026/qr",
        "qr_code_svg_url": "https://yourbusiness.heldsway.com/r/summer2026/qr?format=svg",
        "type": "product",
        "metadata": null,
        "location_id": null,
        "location_name": null,
        "product_id": null,
        "product_name": null,
        "product_slug": null,
        "created_at": "2026-04-01T09:00:00.000000Z"
      }
    ],
    "meta": {
      "current_page": 1,
      "per_page": 20,
      "total": 45,
      "last_page": 3
    }
  }
}

List referral links belonging to a specific affiliate.

GET /v1/affiliates/{affiliateId}/links

Required scope: affiliates:read Rate limit: 120 requests/minute

Path Parameters

Parameter Type Description
affiliateId integer Affiliate ID

Query Parameters

Same as List All Links, excluding affiliate_id.

Example Request

curl "https://api.heldsway.com/api/v1/affiliates/42/links" \
  -H "Authorization: Bearer <access_token>"

Success Response — 200 OK

Same structure as List All Links.

Error Responses

Status Code Description
404 NOT_FOUND Affiliate not found for this business

Returns aggregate statistics for all referral links in your business.

GET /v1/links/stats

Required scope: affiliates:read Rate limit: 120 requests/minute

Query Parameters

Parameter Type Default Description
type string product Link category filter: product (product/landing links), affiliate (store links), or all

Example Request

curl "https://api.heldsway.com/api/v1/links/stats" \
  -H "Authorization: Bearer <access_token>"

Success Response — 200 OK

{
  "success": true,
  "message": "OK",
  "data": {
    "total": 42,
    "active": 38,
    "inactive": 4,
    "total_clicks": 1523
  }
}

Response Fields

Field Type Description
total integer Total number of referral links (active + inactive)
active integer Links with is_active = true
inactive integer Links with is_active = false
total_clicks integer Sum of click_count across all links

Note: Soft-deleted links are excluded from all counts. total_clicks is derived from the denormalized click_count field on each link, which is incremented in real time by the click tracking system.


Create a new referral link for a specific affiliate.

POST /v1/affiliates/{affiliateId}/links

Required scope: affiliates:write Rate limit: 60 requests/minute

Path Parameters

Parameter Type Description
affiliateId integer Affiliate ID

Request Body

Field Type Required Constraints Description
destination_url string Yes valid URL, max 2048 Where visitors are redirected after click tracking
slug string No alphanumeric + hyphens/underscores, max 64, unique Custom URL slug. Auto-generated if omitted
campaign string No max 255 Campaign label for grouping and reporting
is_active boolean No — Defaults to true
metadata object No — Arbitrary key-value data for your own use
discount_code_id integer No exists in business ID of an existing discount code to attach to this link
coupon_code string No max 50, [A-Za-z0-9_-] Create and attach a new coupon code inline
coupon_type string Required with coupon_code percentage | fixed Coupon calculation type
coupon_value number Required with coupon_code >= 0.01 (max 100 for percentage) Discount amount and affiliate commission rate
coupon_min_order number No >= 0 Minimum order subtotal required to apply coupon
coupon_max_uses integer No >= 1 Maximum redemptions permitted
coupon_expires_at datetime No future date Coupon expiration timestamp
location_id string No max 64 Store location identifier
location_name string No max 255 Store location name
product_id string No max 64 Target product ID for product-specific referral link
product_name string No max 255 Target product name

Example Request

curl -X POST "https://api.heldsway.com/api/v1/affiliates/42/links" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_url": "https://example.com/promo",
    "slug": "jane-promo",
    "campaign": "Spring 2026",
    "metadata": { "source": "newsletter" }
  }'

Success Response — 201 Created

{
  "success": true,
  "message": "Created",
  "data": {
    "id": 11,
    "affiliate_id": 42,
    "affiliate_user": { "name": "Jane Smith", "email": "jane@example.com" },
    "slug": "jane-promo",
    "destination_url": "https://example.com/promo",
    "campaign": "Spring 2026",
    "discount_code": null,
    "is_active": true,
    "click_count": 0,
    "conversion_count": 0,
    "tracking_url": "https://yourbusiness.heldsway.com/r/jane-promo",
    "qr_code_url": "https://yourbusiness.heldsway.com/r/jane-promo/qr",
    "qr_code_svg_url": "https://yourbusiness.heldsway.com/r/jane-promo/qr?format=svg",
    "type": "product",
    "metadata": { "source": "newsletter" },
    "location_id": null,
    "location_name": null,
    "product_id": null,
    "product_name": null,
    "product_slug": null,
    "created_at": "2026-04-13T10:00:00.000000Z"
  }
}

Error Responses

Status Code Description
404 NOT_FOUND Affiliate not found for this business
422 — Invalid fields or slug already taken. Form Request failure: Laravel’s standard validation body, no code (see Error Reference → Validation Errors (422))

Create referral links for multiple products in a single request. Links are created asynchronously via a queue job — the endpoint returns immediately with a batch_id for polling.

No limit on the number of items per request (the queue handles scale).

POST /v1/affiliates/{affiliateId}/links?bulk_create=true

Required scope: affiliates:write Rate limit: 60 requests/minute

Path Parameters

Parameter Type Description
affiliateId integer Affiliate ID

Query Parameters

Parameter Type Required Description
bulk_create string Yes Must be "true" to activate bulk mode

Request Body

Field Type Required Constraints Description
links array Yes min 1 item Array of link objects to create
links[].destination_url string Yes valid URL, max 2048 Where visitors are redirected
links[].slug string No alphanumeric + hyphens/underscores, max 64, unique, distinct within batch Custom URL slug. Auto-generated if omitted
links[].campaign string No max 255 Campaign label for grouping
links[].is_active boolean No — Defaults to true
links[].metadata object No — Arbitrary key-value data
links[].discount_code_id integer No exists in business ID of existing coupon to attach
links[].coupon_code string No max 50, unique Inline coupon code to create and attach
links[].coupon_type string Required with coupon_code percentage | fixed Coupon type
links[].coupon_value number Required with coupon_code >= 0.01 (max 100 for percentage) Coupon value
links[].coupon_min_order number No >= 0 Minimum order amount
links[].coupon_max_uses integer No >= 1 Maximum redemptions
links[].coupon_expires_at datetime No future date Coupon expiry date
links[].location_id string No max 64 Store location identifier
links[].location_name string No max 255 Store location name
links[].product_id string No max 64 Target product ID
links[].product_name string No max 255 Target product name

Example Request

curl -X POST "https://api.heldsway.com/api/v1/affiliates/42/links?bulk_create=true" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "links": [
      {
        "destination_url": "https://rentmy.com/product/kayak",
        "campaign": "Summer 2026"
      },
      {
        "destination_url": "https://rentmy.com/product/tent",
        "slug": "tent-deal",
        "campaign": "Summer 2026"
      },
      {
        "destination_url": "https://rentmy.com/product/bike",
        "campaign": "Summer 2026",
        "metadata": { "product_id": "bike-001" }
      }
    ]
  }'

Accepted Response — 202 Accepted

The request was accepted and queued for processing.

{
  "success": true,
  "message": "Bulk creation queued.",
  "data": {
    "batch_id": 17,
    "status": "pending",
    "total_items": 3,
    "status_url": "/v1/affiliates/42/link-batches/17"
  }
}

Error Responses

Status Code Description
404 NOT_FOUND Affiliate not found for this business
422 VALIDATION_ERROR Empty array, missing destination_url, duplicate slugs within batch

Create referral links across every affiliate of a business in a single API call. Each affiliate receives the full set of provided link templates. Processing is asynchronous via a fan-out worker pattern that distributes work into per-affiliate child jobs.


Overview

This endpoint is intended for partner platforms and business operators that need to roll out a set of destination URLs (and optional per-affiliate coupons) to their entire affiliate roster — for example, when launching a new product, refreshing campaigns, or onboarding affiliates to an existing catalog.

Key characteristics

Property Value
Path POST /v1/links/bulk-all-affiliates
Authentication Bearer access token
Scope required affiliates:write
Rate limit 60 req/min (shared with affiliate-write group)
Processing model Asynchronous (queued)
Response 202 Accepted with batch_id
Affiliates per request Determined by affiliate_filter (default: all active)
Link templates per request 1 – 50
Total items hard limit 50,000 (affiliates × templates)
Status polling GET /v1/link-batches/{batchId}

For single-affiliate bulk creation, use [POST /v1/affiliates/{affiliateId}/links?bulk_create=true](/docs/api/partner-bulk-referral-links). This endpoint is a separate, additive flow and does not replace it.


Workflow

Client                    API                          Queue
  │                        │                             │
  │  POST /links/bulk-all-affiliates                     │
  │ ───────────────────────►                             │
  │                        │                             │
  │                        │  Create ReferralLinkBatch   │
  │                        │  (scope=all_affiliates)     │
  │                        │                             │
  │                        │  Dispatch fan-out job ─────►│
  │                        │                             │
  │ ◄─────────────────────  202 + batch_id              │
  │                                                      │
  │                                                      │  Fan-out:
  │                                                      │  chunkById(50)
  │                                                      │  dispatch N
  │                                                      │  child jobs
  │                                                      │
  │                                                      │  Child job per
  │                                                      │  affiliate:
  │                                                      │  - expand
  │                                                      │    templates
  │                                                      │  - resolve
  │                                                      │    coupons
  │                                                      │  - create links
  │                                                      │  - increment
  │                                                      │    counters
  │                                                      │
  │  GET /link-batches/{id} (poll)                       │
  │ ───────────────────────►                             │
  │ ◄─────────────────────  status + counts             │

Prerequisites

Requirement Details
API credentials Issued via POST /v1/auth/token
Affiliates exist At least one active affiliate under the tenant
Scope Access token must carry affiliates:write
Optional coupon templates Use coupon_code_prefix (not raw coupon_code) — see Coupon Templates

Endpoint

POST /v1/links/bulk-all-affiliates

Headers

Authorization: Bearer <access_token>
Content-Type: application/json

Request body

{
  "affiliate_filter": {
    "status": "active",
    "search": "vip"
  },
  "links": [
    {
      "destination_url": "https://example.com/products/widget",
      "campaign": "spring-2026",
      "is_active": true,
      "metadata": { "category": "widgets", "priority": 1 },
      "coupon_code_prefix": "SPRING",
      "coupon_type": "percentage",
      "coupon_value": 15.00,
      "coupon_min_order": 50.00,
      "coupon_max_uses": 100,
      "coupon_expires_at": "2026-06-30T23:59:59Z"
    },
    {
      "destination_url": "https://example.com/products/gadget",
      "campaign": "spring-2026"
    }
  ]
}

Field reference

Field Type Required Constraints Description
affiliate_filter object No — Optional affiliate filter. Defaults to {status: "active"} if omitted.
affiliate_filter.status string No active Affiliate status to target. Currently only active accepted.
affiliate_filter.search string No max: 255 Case-insensitive match against affiliate referral_code, user name, or user email.
location_id string No max: 64 Store location ID applied to every generated link in the batch.
location_name string No max: 255 Store location name applied to every generated link in the batch.
links array Yes min: 1, max: 50 Array of link templates. Each template is applied to every matching affiliate.
links[].product_id string No max: 64 Target product ID for this link template.
links[].product_name string No max: 255 Target product name for this link template.
links[].destination_url string Yes valid URL, max: 2048 Target URL for the referral link.
links[].campaign string No max: 255 Optional campaign label.
links[].is_active boolean No default true Whether the generated links start active.
links[].metadata object No — Arbitrary JSON metadata.
links[].discount_code_id integer No must exist under tenant Attach an existing discount code. Mutually exclusive with coupon_code_prefix.
links[].coupon_code_prefix string No max: 30, [A-Za-z0-9_-] Prefix used to mint a per-affiliate coupon code. See Coupon Templates.
links[].coupon_type string Required with coupon_code_prefix percentage | fixed Coupon type.
links[].coupon_value number Required with coupon_code_prefix >= 0.01 Coupon value.
links[].coupon_min_order number No >= 0 Minimum order amount for the coupon.
links[].coupon_max_uses integer No >= 1 Optional max-uses cap.
links[].coupon_expires_at datetime No must be in the future Coupon expiry timestamp.

Rejected fields

These are explicitly rejected by validation — supplying them returns 422:

Field Reason
links[].slug Slugs are globally unique. Auto-generated per affiliate to avoid collision.
links[].coupon_code Raw coupon codes are unique per business and cannot be shared across affiliates. Use coupon_code_prefix instead.

Coupon Templates

When coupon_code_prefix is supplied, a unique coupon code is materialized per affiliate at processing time:

final_code = "{coupon_code_prefix}-{AFFILIATE_REFERRAL_CODE}"

For example, prefix SPRING with three affiliates whose referral_codes are ABC123, DEF456, GHI789 yields three coupons: SPRING-ABC123, SPRING-DEF456, SPRING-GHI789. Each coupon is owned by its respective affiliate.

Coupons are created via firstOrCreate keyed on (business_id, code) — running the same prefix twice is idempotent and does not produce duplicates.


Response

Success — 202 Accepted

{
  "success": true,
  "message": "Bulk creation queued for all affiliates.",
  "data": {
    "batch_id": 1284,
    "scope": "all_affiliates",
    "status": "pending",
    "total_affiliates": 312,
    "total_items": 624,
    "status_url": "/v1/link-batches/1284"
  }
}
Field Description
batch_id Identifier used for polling.
scope Always all_affiliates for this endpoint.
status pending immediately after dispatch. Transitions to processing, then completed (or failed).
total_affiliates Snapshot of matched affiliate count at request time.
total_items total_affiliates × len(links). Used for progress reporting.
status_url Convenience path to poll for results, relative to the Base URL (https://api.heldsway.com/api): the code returns /v1/... without the /api prefix.

Validation error — 422 Unprocessable Entity

Returned for any validation failure (rule violation, exceeded limit, no matching affiliates).

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": {
      "links.0.coupon_code": [
        "Use coupon_code_prefix instead. coupon_code is unique per business and cannot be shared across affiliates."
      ]
    }
  }
}

Other 422 codes:

Code Meaning
NO_AFFILIATES No affiliates match the filter criteria.
LIMIT_EXCEEDED total_affiliates × len(links) exceeds the 50,000 hard limit.
VALIDATION_ERROR One or more fields failed validation. See details for per-field messages.

Server error — 500 Internal Server Error

{
  "success": false,
  "error": {
    "code": "SERVER_ERROR",
    "message": "Failed to queue bulk link creation."
  }
}

Polling for Status

Headers

Authorization: Bearer <access_token>

Scope: affiliates:read.

Response — 200 OK

While processing:

{
  "success": true,
  "data": {
    "batch_id": 1284,
    "scope": "all_affiliates",
    "status": "processing",
    "total_items": 624,
    "created_count": 412,
    "error_count": 0,
    "total_affiliates": 312,
    "processed_affiliates": 206,
    "created_at": "2026-05-15T10:42:18Z",
    "completed_at": null
  }
}

On completion:

{
  "success": true,
  "data": {
    "batch_id": 1284,
    "scope": "all_affiliates",
    "status": "completed",
    "total_items": 624,
    "created_count": 622,
    "error_count": 2,
    "total_affiliates": 312,
    "processed_affiliates": 312,
    "created_at": "2026-05-15T10:42:18Z",
    "completed_at": "2026-05-15T10:44:31Z",
    "results": [
      {
        "affiliate_id": 42,
        "created": [9011, 9012],
        "errors": []
      },
      {
        "affiliate_id": 43,
        "created": [9013],
        "errors": [
          {
            "index": 1,
            "destination_url": "https://example.com/products/gadget",
            "message": "The slug \"abc12345\" is already taken."
          }
        ]
      }
    ],
    "errors": null
  }
}

Status values

Status Meaning
pending Batch row created, fan-out job dispatched but not yet executed.
processing Fan-out completed; child jobs are running or queued.
completed All affiliates processed. created_count + error_count may be inspected per affiliate via results.
failed Fan-out step failed before children could be dispatched. errors contains the failure reason.

Polling guidelines

  • Poll no more than once every 2–5 seconds. The endpoint is rate-limited at 120 req/min within the affiliates:read group.
  • Use processed_affiliates / total_affiliates as a progress ratio.
  • A non-zero error_count does not imply batch failure — individual link errors are captured per affiliate without aborting the batch.

Not found — 404 Not Found

Returned if the batch ID does not exist or belongs to a different tenant.

{
  "success": false,
  "error": { "code": "NOT_FOUND", "message": "Batch not found." }
}

Worked Example

1. Submit batch

curl -X POST "https://api.heldsway.com/api/v1/links/bulk-all-affiliates" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "affiliate_filter": { "status": "active" },
    "links": [
      {
        "destination_url": "https://example.com/products/widget",
        "campaign": "spring-2026",
        "coupon_code_prefix": "SPRING",
        "coupon_type": "percentage",
        "coupon_value": 15.00
      },
      {
        "destination_url": "https://example.com/products/gadget",
        "campaign": "spring-2026"
      }
    ]
  }'

2. Response

{
  "success": true,
  "message": "Bulk creation queued for all affiliates.",
  "data": {
    "batch_id": 1284,
    "scope": "all_affiliates",
    "status": "pending",
    "total_affiliates": 312,
    "total_items": 624,
    "status_url": "/v1/link-batches/1284"
  }
}

3. Poll until complete

while true; do
  STATUS=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
    "https://api.heldsway.com/api/v1/link-batches/1284" \
    | jq -r '.data.status')

  echo "Status: $STATUS"
  [[ "$STATUS" == "completed" || "$STATUS" == "failed" ]] && break
  sleep 3
done

4. Inspect per-affiliate results

curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
  "https://api.heldsway.com/api/v1/link-batches/1284" \
  | jq '.data.results[] | select(.errors | length > 0)'

Behavior Notes

Idempotency

  • Coupon resolution is idempotent: re-submitting the same coupon_code_prefix will reuse the existing per-affiliate DiscountCode rather than creating duplicates.
  • Link creation is not idempotent. Each submission produces a fresh set of referral links with newly generated slugs. Submitting the same payload twice doubles the number of links.

Tenant isolation

  • The endpoint always operates on the affiliates of the access token’s tenant.
  • Cross-tenant affiliate access is impossible: filters and queries are bound to business_id.

Partial success

  • A child job that fails to create one or more links does not abort the batch. Failures are captured in the per-affiliate errors[] array with index, destination_url, and message.
  • A child job that fails entirely (e.g. a database connection error) is recorded under that affiliate’s errors array as a single { "message": "Job failed: ..." } entry. The batch still progresses.

Concurrency

  • Child jobs run in parallel across queue workers.
  • Counter updates (created_count, error_count, processed_affiliates) use atomic SQL increments to remain consistent under parallel execution.
  • The first child to observe processed_affiliates >= total_affiliates transitions the batch to completed.

Limits

Limit Value Behavior on breach
Link templates per request 50 422 VALIDATION_ERROR
Total items (affiliates × templates) 50,000 422 LIMIT_EXCEEDED
Affiliate filter Active affiliates only Inactive/pending/suspended/terminated excluded

Comparison: Single-Affiliate vs All-Affiliates

Aspect Single affiliate (?bulk_create=true) All affiliates (this endpoint)
Path POST /v1/affiliates/{id}/links POST /v1/links/bulk-all-affiliates
Scope of effect One affiliate Many affiliates (filterable)
Allowed slug Yes (per link) No (auto-generated)
Coupon input Raw coupon_code Templated via coupon_code_prefix
Job topology Single worker job Fan-out + per-affiliate child jobs
Status endpoint GET /v1/affiliates/{id}/link-batches/{batchId} GET /v1/link-batches/{batchId}
Response code 202 Accepted 202 Accepted

The unified GET /v1/link-batches/{batchId} endpoint works for batches of both scopes.


Error Reference

HTTP Code Cause
401 UNAUTHORIZED Missing or invalid bearer token
402 SUBSCRIPTION_REQUIRED The business has no active subscription (see Error Reference → Subscription Required (402))
403 FORBIDDEN Token lacks affiliates:write scope
422 VALIDATION_ERROR One or more fields failed validation
422 NO_AFFILIATES Filter matched zero affiliates
422 LIMIT_EXCEEDED Batch size exceeds 50,000 items
429 RATE_LIMIT_EXCEEDED Exceeded 60 req/min on the write group
500 SERVER_ERROR Unexpected failure queueing the batch

Changelog

Date Change
2026-05-15 Initial release.

Get Batch Status

Poll for the status and results of a bulk link creation batch.

GET /v1/affiliates/{affiliateId}/link-batches/{batchId}

Required scope: affiliates:read Rate limit: 120 requests/minute

Path Parameters

Parameter Type Description
affiliateId integer Affiliate ID
batchId integer Batch ID returned from bulk create

Example Request

curl "https://api.heldsway.com/api/v1/affiliates/42/link-batches/17" \
  -H "Authorization: Bearer <access_token>"

Status Values

Status Meaning
pending Queued, not yet started
processing Currently creating links
completed Done — check created_count and error_count
failed Job failed entirely — check errors

Response — Processing (200 OK)

{
  "success": true,
  "message": "OK",
  "data": {
    "batch_id": 17,
    "status": "processing",
    "total_items": 3,
    "created_count": 0,
    "error_count": 0,
    "created_at": "2026-04-14T10:00:00.000000Z",
    "completed_at": null
  }
}

Response — Completed (200 OK)

{
  "success": true,
  "message": "OK",
  "data": {
    "batch_id": 17,
    "status": "completed",
    "total_items": 3,
    "created_count": 2,
    "error_count": 1,
    "created_at": "2026-04-14T10:00:00.000000Z",
    "completed_at": "2026-04-14T10:00:05.000000Z",
    "created_link_ids": [101, 102],
    "created_links": [
      {
        "id": 101,
        "affiliate_id": 42,
        "affiliate_user": { "name": "Jane Smith", "email": "jane@example.com" },
        "slug": "a1b2c3d4",
        "destination_url": "https://rentmy.com/product/kayak",
        "campaign": "Summer 2026",
        "discount_code": null,
        "is_active": true,
        "click_count": 0,
        "conversion_count": 0,
        "tracking_url": "https://yourbusiness.heldsway.com/r/a1b2c3d4",
        "qr_code_url": "https://yourbusiness.heldsway.com/r/a1b2c3d4/qr",
        "qr_code_svg_url": "https://yourbusiness.heldsway.com/r/a1b2c3d4/qr?format=svg",
        "type": "product",
        "metadata": null,
        "location_id": null,
        "location_name": null,
        "product_id": null,
        "product_name": null,
        "product_slug": "kayak",
        "created_at": "2026-04-14T10:00:02.000000Z"
      },
      {
        "id": 102,
        "affiliate_id": 42,
        "affiliate_user": { "name": "Jane Smith", "email": "jane@example.com" },
        "slug": "tent-deal",
        "destination_url": "https://rentmy.com/product/tent",
        "campaign": "Summer 2026",
        "discount_code": null,
        "is_active": true,
        "click_count": 0,
        "conversion_count": 0,
        "tracking_url": "https://yourbusiness.heldsway.com/r/tent-deal",
        "qr_code_url": "https://yourbusiness.heldsway.com/r/tent-deal/qr",
        "qr_code_svg_url": "https://yourbusiness.heldsway.com/r/tent-deal/qr?format=svg",
        "type": "product",
        "metadata": null,
        "location_id": null,
        "location_name": null,
        "product_id": null,
        "product_name": null,
        "product_slug": "tent",
        "created_at": "2026-04-14T10:00:03.000000Z"
      }
    ],
    "errors": [
      {
        "index": 2,
        "destination_url": "https://rentmy.com/product/bike",
        "message": "The slug \"bike-deal\" is already taken."
      }
    ]
  }
}

Error Responses

Status Code Description
404 NOT_FOUND Batch not found for this business/affiliate

:::warning Not currently available. The link-update endpoint is disabled: its route is commented out in modules/AffiliateSupport/Routes/api.php. To change a link’s status use the status endpoint, and to attach a coupon use the coupon endpoint (both under Referral Links). No other link field can be changed through the API. :::


Soft-delete a referral link. The tracking URL will stop working. Historical click data is preserved.

DELETE /v1/links/{id}

Required scope: affiliates:write Rate limit: 60 requests/minute

Path Parameters

Parameter Type Description
id integer Referral link ID

Example Request

curl -X DELETE "https://api.heldsway.com/api/v1/links/11" \
  -H "Authorization: Bearer <access_token>"

Success Response — 204 No Content

Empty body.

Error Responses

Status Code Description
404 NOT_FOUND Link not found for this business

Activate or deactivate a referral link (source: ReferralLinkApiController::updateStatus()). This is the only way to change a link’s is_active flag through the API.

POST /v1/links/{id}/status

Required scope: affiliates:write Rate limit: 60 requests/minute

Path Parameters

Parameter Type Description
id integer Referral link ID

Request Body

Rules from UpdateReferralLinkStatusApiRequest::rules().

Field Type Required Description
is_active boolean Yes true to activate the link, false to deactivate it

Example Request

curl -X POST "https://api.heldsway.com/api/v1/links/11/status" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'

Success Response — 200 OK

The updated link, in the shape built by ReferralLinkApiController::formatLink():

{
  "success": true,
  "message": "Referral link status updated.",
  "data": {
    "id": 11,
    "affiliate_id": 42,
    "affiliate_user": { "name": "Jane Smith", "email": "jane@example.com" },
    "slug": "jane-promo",
    "destination_url": "https://example.com/promo",
    "campaign": "Spring 2026",
    "discount_code": null,
    "is_active": false,
    "click_count": 18,
    "conversion_count": 3,
    "tracking_url": "https://yourbusiness.heldsway.com/r/jane-promo",
    "qr_code_url": "https://yourbusiness.heldsway.com/r/jane-promo/qr",
    "qr_code_svg_url": "https://yourbusiness.heldsway.com/r/jane-promo/qr?format=svg",
    "type": "product",
    "metadata": { "source": "newsletter" },
    "location_id": null,
    "location_name": null,
    "product_id": null,
    "product_name": null,
    "product_slug": null,
    "created_at": "2026-04-13T10:00:00.000000Z"
  }
}

discount_code, when a coupon is attached, is an object with id, code, type, value, min_order, max_uses and expires_at (see Attach Coupon to Referral Link below).

Error Responses

Status Code Description
404 NOT_FOUND Link not found for this business
422 — is_active missing or not a boolean. Form Request failure: Laravel’s standard validation body, no code (see Error Reference → Validation Errors (422))

Create a new discount code bound to the link’s affiliate and attach it to the link (source: ReferralLinkApiController::attachCoupon() → LinkCouponService::attach()). The coupon is always created by this call; an existing code cannot be attached. If the link already has a coupon, the call fails with COUPON_EXISTS unless replace is true.

POST /v1/links/{id}/coupon

Required scope: affiliates:write Rate limit: 60 requests/minute

Path Parameters

Parameter Type Description
id integer Referral link ID

Request Body

Rules from AttachLinkCouponApiRequest::rules().

Field Type Required Description
coupon_code string Yes Max 50 characters; letters, numbers, - and _ only. Must not already exist in this business, including soft-deleted codes
coupon_type string Yes percentage or fixed
coupon_value number Yes Minimum 0.01; maximum 100 when coupon_type is percentage
coupon_min_order number No Minimum 0. Stored as 0 when omitted
coupon_max_uses integer No Minimum 1. null = unlimited
coupon_expires_at date No Must be in the future
replace boolean No Must be true to replace a coupon the link already has

Behaviour

  • The new code is created active, for the business, bound to the link’s affiliate, and the link’s discount_code_id is pointed at it — all in one transaction.
  • When a coupon is replaced, the previous coupon is deactivated only if no other link still uses it. previous_coupon.deactivated and previous_coupon.shared_link_count report what happened.

Example Request

curl -X POST "https://api.heldsway.com/api/v1/links/11/coupon" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "coupon_code": "JANE15",
    "coupon_type": "percentage",
    "coupon_value": 15,
    "coupon_max_uses": 100,
    "replace": true
  }'

Success Response — 201 Created

The Referral Link Object with the new coupon in discount_code, plus previous_coupon (null when the link had no coupon):

{
  "success": true,
  "message": "Created",
  "data": {
    "id": 11,
    "affiliate_id": 42,
    "affiliate_user": { "name": "Jane Smith", "email": "jane@example.com" },
    "slug": "jane-promo",
    "destination_url": "https://example.com/promo",
    "campaign": "Spring 2026",
    "discount_code": {
      "id": 57,
      "code": "JANE15",
      "type": "percentage",
      "value": 15,
      "min_order": 0,
      "max_uses": 100,
      "expires_at": null
    },
    "is_active": true,
    "click_count": 18,
    "conversion_count": 3,
    "tracking_url": "https://yourbusiness.heldsway.com/r/jane-promo",
    "qr_code_url": "https://yourbusiness.heldsway.com/r/jane-promo/qr",
    "qr_code_svg_url": "https://yourbusiness.heldsway.com/r/jane-promo/qr?format=svg",
    "type": "product",
    "metadata": { "source": "newsletter" },
    "location_id": null,
    "location_name": null,
    "product_id": null,
    "product_name": null,
    "product_slug": null,
    "created_at": "2026-04-13T10:00:00.000000Z",
    "previous_coupon": {
      "id": 31,
      "code": "JANE10",
      "deactivated": true,
      "shared_link_count": 0
    }
  }
}

Error Responses

Status Code Description
404 NOT_FOUND Link not found for this business
409 COUPON_EXISTS The link already has a coupon and replace was not true. error.details.current_coupon holds that coupon: id, code, type, value, min_order, max_uses, expires_at, is_active, and shared_link_count (how many other links use it)
409 AFFILIATE_UNAVAILABLE The link’s affiliate has been deleted, so a coupon cannot be bound to it
422 — Invalid coupon fields, or coupon_code already exists in this business. Form Request failure: Laravel’s standard validation body, no code (see Error Reference → Validation Errors (422))
500 SERVER_ERROR Unexpected failure creating or attaching the coupon

Soft-delete up to 500 referral links in one request (source: ReferralLinkApiController::bulkDestroy() → ReferralLinkService::bulkDelete()). IDs that do not exist or belong to another business are skipped; compare deleted_count with requested_count. Any affiliate whose primary link was deleted has that reference cleared.

POST /v1/links/bulk-delete

Required scope: affiliates:write Rate limit: 60 requests/minute

Request Body

Rules from BulkDeleteReferralLinksApiRequest::rules().

Field Type Required Description
ids integer[] Yes 1–500 referral link IDs, no duplicates

Example Request

curl -X POST "https://api.heldsway.com/api/v1/links/bulk-delete" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{ "ids": [11, 12, 999] }'

Success Response — 200 OK

{
  "success": true,
  "message": "2 referral link(s) deleted.",
  "data": {
    "deleted_count": 2,
    "requested_count": 3
  }
}

Error Responses

Status Code Description
422 — ids missing, empty, over 500 entries, non-integer or duplicate values. Form Request failure: Laravel’s standard validation body, no code (see Error Reference → Validation Errors (422))
500 SERVER_ERROR Unexpected failure while deleting