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 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 Links for Affiliate
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 |
Get Link Stats
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_clicksis derived from the denormalizedclick_countfield on each link, which is incremented in real time by the click tracking system.
Create Referral Link
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)) |
Bulk Create Referral Links
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 |
Bulk Create Referral Links — All Affiliates
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
GET /v1/link-batches/{batchId}
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:readgroup. - Use
processed_affiliates / total_affiliatesas a progress ratio. - A non-zero
error_countdoes 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_prefixwill reuse the existing per-affiliateDiscountCoderather 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 withindex,destination_url, andmessage. - A child job that fails entirely (e.g. a database connection error) is recorded under that affiliate’s
errorsarray 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_affiliatestransitions the batch tocompleted.
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 |
Update Referral Link
:::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.
:::
Delete Referral Link
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 |
Update Referral Link Status
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)) |
Attach Coupon to Referral Link
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_idis 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.deactivatedandprevious_coupon.shared_link_countreport 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 |
Bulk Delete Referral Links
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 |