HeldSway

Partner Business Registration API

Register a new business on HeldSway from an authorized external partner platform. The request payload is encrypted using a shared secret for secure server-to-server communication.


Prerequisites

The partner endpoints in this part (Check Business, Register Business, Set Password) are available exclusively to authorized partners with a pre-shared PARTNER_TOKEN. Each route uses partner.auth:rentmy and throttle:10,1.

Base URL: every path in this document is the full path on the API host, https://api.heldsway.com (); for example POST /api/v1/partner/check-business is https://api.heldsway.com/api/v1/partner/check-business.

Requirement Details
Authentication Authorization: Bearer <PARTNER_TOKEN> header
Encryption Request body must be AES-256-CBC encrypted using the same PARTNER_TOKEN
Rate Limit 10 requests per minute per IP

Encryption Specification

All request payloads must be encrypted before transmission. This provides a dual-layer security model: the Bearer token authenticates the caller, and the encrypted payload ensures data integrity and confidentiality.

Algorithm

Parameter Value
Cipher AES-256-CBC
Key Derivation SHA-256(PARTNER_TOKEN) — produces a 32-byte encryption key
IV 16 bytes, randomly generated per request
Integrity HMAC-SHA256 over iv_bytes + ciphertext_bytes using the derived key

Encryption Steps

  1. Derive the key: key = SHA-256(PARTNER_TOKEN) (binary, 32 bytes)
  2. Generate IV: 16 random bytes
  3. Encrypt: ciphertext = AES-256-CBC(JSON_payload, key, iv) using OPENSSL_RAW_DATA flag
  4. Compute MAC: mac = HMAC-SHA256(iv_bytes + ciphertext_bytes, key) — hex-encoded, 64 characters
  5. Encode: Base64-encode both ciphertext and iv

Reference Implementation (PHP)

$token = 'your-partner-token';
$key   = hash('sha256', $token, binary: true);
$iv    = random_bytes(16);

$plaintext  = json_encode([
    'business_name' => 'Acme Rentals',
    'owner_name'    => 'John Doe',
    'email'         => 'john@acme.com',
    'phone'         => '+1234567890',
    'address'       => '123 Main St, City, ST 12345',
    'website_url'   => 'https://acme.com',
]);

$ciphertext = openssl_encrypt($plaintext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
$mac        = hash_hmac('sha256', $iv . $ciphertext, $key);

$requestBody = [
    'payload' => base64_encode($ciphertext),
    'iv'      => base64_encode($iv),
    'mac'     => $mac,
];

Reference Implementation (Node.js)

const crypto = require('crypto');

const token = 'your-partner-token';
const key   = crypto.createHash('sha256').update(token).digest(); // 32-byte Buffer

const data = JSON.stringify({
  business_name: 'Acme Rentals',
  owner_name:    'John Doe',
  email:         'john@acme.com',
  phone:         '+1234567890',
  address:       '123 Main St, City, ST 12345',
  website_url:   'https://acme.com',
});

const iv         = crypto.randomBytes(16);
const cipher     = crypto.createCipheriv('aes-256-cbc', key, iv);
const ciphertext = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
const mac        = crypto.createHmac('sha256', key).update(Buffer.concat([iv, ciphertext])).digest('hex');

const requestBody = {
  payload: ciphertext.toString('base64'),
  iv:      iv.toString('base64'),
  mac:     mac,
};

Reference Implementation (Python)

import hashlib, hmac, json, os, base64
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding

token = "your-partner-token"
key   = hashlib.sha256(token.encode()).digest()  # 32 bytes

data = json.dumps({
    "business_name": "Acme Rentals",
    "owner_name":    "John Doe",
    "email":         "john@acme.com",
    "phone":         "+1234567890",
    "address":       "123 Main St, City, ST 12345",
    "website_url":   "https://acme.com",
}).encode()

# PKCS7 padding (AES-CBC requires block-aligned input)
padder  = padding.PKCS7(128).padder()
padded  = padder.update(data) + padder.finalize()

iv         = os.urandom(16)
cipher     = Cipher(algorithms.AES(key), modes.CBC(iv))
encryptor  = cipher.encryptor()
ciphertext = encryptor.update(padded) + encryptor.finalize()

mac = hmac.new(key, iv + ciphertext, hashlib.sha256).hexdigest()

request_body = {
    "payload": base64.b64encode(ciphertext).decode(),
    "iv":      base64.b64encode(iv).decode(),
    "mac":     mac,
}

Check Business

Non-mutating preflight for the two-phase connect flow (source: App\Http\Controllers\Api\V1\Partner\CheckBusinessController → PartnerRegistrationService::checkBusiness()). It decrypts and validates the same payload Register Business takes, then reports whether a HeldSway business already exists for the owner email. Show the result to the user, then call Register Business with the action they choose. Nothing is created or changed.

POST /api/v1/partner/check-business
Authorization: Bearer <PARTNER_TOKEN>
Content-Type: application/json

Auth: partner.auth:rentmy (partner Bearer token, no scope) Rate limit: 10 requests/minute per IP (throttle:10,1)

Request Body

Rules from CheckBusinessRequest::rules().

Field Type Required Description
payload string Yes Base64-encoded AES-256-CBC ciphertext (see Encryption Specification)
iv string Yes Base64-encoded 16-byte initialization vector
mac string Yes 64-character hex HMAC-SHA256 digest
store_id string No Max 64. Unauthenticated duplicate of the payload field; a JSON integer is converted to a string. The payload value wins
location_id string No Max 64. Same rules as store_id
location_name string No Max 255. Unauthenticated duplicate of the payload field; the payload value wins

store_id, location_id and location_name are also read from the query string. For each one the encrypted payload wins, then the JSON body, then the query string.

Decrypted Payload Fields

Validated after decryption by PartnerRegistrationService::validateBusinessData() (shared with Register Business).

Field Type Required Constraints
business_name string Yes max 255
owner_name string Yes max 255
email string Yes valid email, max 255
phone string No max 50
address string No max 500
website_url string Yes valid URL, max 255
store_id string No max 64 (a JSON integer is converted to a string)
location_id string No max 64 (a JSON integer is converted to a string)
location_name string No max 255

Success Response — 200 OK (no existing business)

{
  "success": true,
  "message": "OK",
  "data": {
    "exists": false,
    "business_summary": null,
    "user_exists": true,
    "data_to_be_shared": {
      "business_name": "Acme Rentals",
      "email": "john@acme.com",
      "owner_name": "John Doe",
      "phone": "+1234567890",
      "address": "123 Main St, City, ST 12345",
      "website_url": "https://acme.com"
    }
  }
}

user_exists is true when a HeldSway user with this email already exists. Register Business with action: "create_new" creates the business under that user.

Success Response — 200 OK (another location for a connected store)

Returned when an active business owned by this email is already bound to the same store_id and the request carries a location_id:

{
  "success": true,
  "message": "OK",
  "data": {
    "exists": true,
    "connected": true,
    "can_add_location": true,
    "store_id": "4821",
    "business_summary": { "name": "Acme Rentals", "slug": "acme-rentals", "status": "active" },
    "locations": [
      { "id": "1", "name": "Downtown" }
    ]
  }
}

locations lists the locations already connected to the business.

Existing business (409)

Any other business owned by this email that passes the blocked-state checks in the Error Reference below (not suspended, same partner and email, same or no store) is reported as EXISTING_INACTIVE_BUSINESS, whether it is active, inactive, disconnected or soft-deleted. The partner can then offer “reconnect” or “create new”. The checks run first (PartnerRegistrationService::guardBlockedStates()): a suspended business returns BUSINESS_LOCKED, a partner or email mismatch returns ALREADY_CONNECTED / PARTNER_MISMATCH / EMAIL_MISMATCH, and a different store returns STORE_MISMATCH.

{
  "success": false,
  "error": {
    "code": "EXISTING_INACTIVE_BUSINESS",
    "message": "Business is inactive. You can reconnect or create a new one.",
    "details": {
      "existing_business": { "name": "Acme Rentals", "slug": "acme-rentals", "status": "disconnected", "deleted": false }
    }
  }
}

Error Reference

Mapped by the controller and MapsPartnerErrors::mapCommonPartnerError().

HTTP Status Error Code Cause
400 DECRYPTION_FAILED Bad base64 in payload or iv, wrong IV length, or MAC mismatch
401 UNAUTHORIZED Missing or invalid partner Bearer token
409 EXISTING_INACTIVE_BUSINESS A business owned by this email exists (see above); details.existing_business describes it
409 ALREADY_CONNECTED The email’s active business was connected by a different partner, or its connected email differs; details.existing_business describes it
409 PARTNER_MISMATCH The email’s (inactive or deleted) business was connected by a different partner
409 EMAIL_MISMATCH The email’s (inactive or deleted) business was connected with a different email
409 BUSINESS_LOCKED The email’s business is suspended
409 STORE_MISMATCH The email’s business is bound to a different store_id
409 ACCOUNT_DEACTIVATED The HeldSway user with this email has been deactivated
422 VALIDATION_ERROR A decrypted payload field is missing or invalid; message “Invalid business data in encrypted payload.”, per-field messages in details
422 — Missing or malformed envelope fields, rejected by CheckBusinessRequest with Laravel’s standard validation body (message + errors), no code
429 — More than 10 requests/minute from this IP. Laravel’s throttle response: {"message": "Too Many Attempts."} with a Retry-After header
500 CHECK_FAILED Unexpected error that is not a RuntimeException
503 PARTNER_DISABLED Partner integration is disabled
503 PARTNER_NOT_CONFIGURED Partner token is not set on the server

A RuntimeException the mapper does not recognise is re-thrown and returns Laravel’s generic 500 body instead of CHECK_FAILED.


Register Business

POST /api/v1/partner/register-business
Authorization: Bearer <PARTNER_TOKEN>
Content-Type: application/json

Request Body (Encrypted Envelope)

Field Type Required Description
payload string Yes Base64-encoded AES-256-CBC ciphertext
iv string Yes Base64-encoded 16-byte initialization vector
mac string Yes 64-character hex HMAC-SHA256 digest
action string Yes reconnect or create_new — the choice the user made after Check Business
consent_given boolean Yes Must be accepted (true)
store_id string No Max 64. Unauthenticated duplicate of the payload field; the payload value wins
location_id string No Max 64. Same rules as store_id
location_name string No Max 255. Same rules as store_id

Rules from RegisterBusinessRequest::rules().

Decrypted Payload Fields

These fields must be JSON-encoded, then encrypted as described above.

Field Type Required Constraints Description
business_name string Yes max 255 chars Name of the business
owner_name string Yes max 255 chars Full name of the business owner
email string Yes valid email, max 255 chars Owner’s email address
phone string No max 50 chars Owner’s phone number
address string No max 500 chars Business address
website_url string Yes valid URL, max 255 chars Business website
store_id string No max 64 chars Partner store ID (a JSON integer is converted to a string)
location_id string No max 64 chars Partner location ID (a JSON integer is converted to a string)
location_name string No max 255 chars Partner location name

Example

curl -X POST https://api.heldsway.com/api/v1/partner/register-business \
  -H "Authorization: Bearer <PARTNER_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": "dGhpcyBpcyBhIGJhc2U2NC1lbmNvZGVkIGNpcGhlcnRleHQ=",
    "iv": "c29tZS1iYXNlNjQtaXY=",
    "mac": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
    "action": "create_new",
    "consent_given": true
  }'

Success Response

HTTP 201 Created

{
  "success": true,
  "message": "Business registered successfully.",
  "data": {
    "linked": false,
    "business": {
      "id": 42,
      "name": "Acme Rentals",
      "slug": "acme-rentals",
      "url": "https://acme-rentals.heldsway.com"
    },
    "credentials": {
      "api_key": "as_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPqRsTu",
      "api_secret": "aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkL",
      "access_token": "xYzAbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGh",
      "token_type": "Bearer",
      "expires_in": 3153600000,
      "scopes": [
        "affiliates:read",
        "affiliates:write",
        "conversions:read",
        "conversions:write",
        "clicks:read",
        "clicks:write",
        "commissions:read",
        "reports:read",
        "sdk:init",
        "pageviews:write",
        "orders:read",
        "orders:write",
        "settings:read",
        "settings:write",
        "connections:read",
        "connections:write",
        "payouts:read",
        "payouts:write"
      ]
    },
    "owner": {
      "email": "john@acme.com",
      "name": "John Doe"
    },
    "password_setup": {
      "token": "q8Zr1vXk…64 characters",
      "expires_in": 900
    }
  }
}

Response Fields

Field Type Description
business.id integer Unique business identifier
business.name string Business display name
business.slug string URL-safe subdomain slug
business.url string Business panel URL from Business::getUrl(): the verified custom domain, else {slug}.{MAIN_DOMAIN}, with the scheme of APP_URL
credentials.api_key string Public API key (prefix: as_). Safe to store.
credentials.api_secret string Sensitive. Shown only once. Store securely.
credentials.access_token string Short-lived Bearer token for immediate API use
credentials.token_type string Always "Bearer"
credentials.expires_in integer Access token lifetime in seconds. Currently 3153600000 (100 years): AccessTokenService::DEFAULT_TTL_SECONDS
credentials.scopes string[] Scopes granted to this API key — every ApiKeyScope value (see module-8 §9.3 enums)
owner.email string Owner’s email address
owner.name string Owner’s display name
linked boolean false when a new business was created (HTTP 201); true when an existing business was reconnected (HTTP 200, message “Existing business linked successfully.”)
password_setup.token string One-time token for Set Password (POST /api/v1/partner/set-password)
password_setup.expires_in integer Seconds until the token expires (account_setup.partner_api_expiry_minutes × 60; default 900)

Important Notes

  • The api_secret is returned only once in this response. It cannot be retrieved again. Store it securely.
  • The access_token lifetime is expires_in seconds (currently 100 years, AccessTokenService::DEFAULT_TTL_SECONDS). Use the api_key and api_secret to issue new tokens via [POST /api/v1/auth/token](/docs/api/heldsway-rest-api-overview#token-issuance).
  • If the owner email already belongs to an existing HeldSway user, the business is created under that existing account.
  • action: "reconnect" reactivates the business owned by this email and keeps its data; with no such business it fails with NO_EXISTING_BUSINESS. action: "create_new" creates a fresh business when none exists; when one exists it wipes that business’s operational data and reactivates it (performOverrideReconnect()). Both reconnect paths return HTTP 200 with linked: true.
  • When an active business owned by this email is already bound to the same store_id and the request carries a location_id, the location is added instead (HTTP 200, message “Location connected successfully.”). That response has status: "location_connected", linked: true, business, location, and credentials with only access_token, token_type, expires_in and scopes. api_key / api_secret appear only when every earlier key had been revoked and a new one was minted. There is no owner and no password_setup.

Error Reference

HTTP Status Error Code Cause
Mapped by RegisterBusinessController and MapsPartnerErrors::mapCommonPartnerError().
HTTP Status Error Code Cause
400 DECRYPTION_FAILED Bad base64 in payload or iv, wrong IV length, or MAC mismatch
401 UNAUTHORIZED Missing or invalid Authorization: Bearer header
404 NO_EXISTING_BUSINESS action: "reconnect" but no business is owned by this email
409 ALREADY_CONNECTED The email’s active business was connected by a different partner, or its connected email differs; details.existing_business describes it
409 PARTNER_MISMATCH The email’s (inactive or deleted) business was connected by a different partner
409 EMAIL_MISMATCH The email’s (inactive or deleted) business was connected with a different email
409 BUSINESS_LOCKED The email’s business is suspended
409 STORE_MISMATCH The email’s business is bound to a different store_id
409 STORE_ALREADY_CONNECTED The store_id is already bound to another business (unique index businesses_partner_store_unique)
409 ACCOUNT_DEACTIVATED The HeldSway user with this email has been deactivated
409 BUSINESS_NAME_TAKEN Creating the business failed because its generated slug is still taken after 3 attempts; change business_name and retry
422 CONSENT_REQUIRED Consent not given. RegisterBusinessRequest already rejects consent_given: false (accepted), so this service-level check is a backstop
422 VALIDATION_ERROR Missing or invalid fields in the decrypted payload; message “Invalid business data in encrypted payload.”, per-field messages in details
422 — Envelope fields rejected by RegisterBusinessRequest: payload, iv, mac (required, mac exactly 64 characters), action (required, reconnect or create_new), consent_given (required, boolean, accepted), store_id / location_id (nullable string, max 64), location_name (nullable string, max 255). Laravel’s standard validation body (message + errors), no code
429 — Rate limit exceeded (10 requests/minute). Laravel’s throttle response: {"message": "Too Many Attempts."} with a Retry-After header
500 REGISTRATION_FAILED Unexpected error that is not a RuntimeException
503 PARTNER_DISABLED Partner integration is disabled
503 PARTNER_NOT_CONFIGURED Partner token is not set on the server

A RuntimeException the mapper does not recognise is re-thrown and returns Laravel’s generic 500 body instead of REGISTRATION_FAILED.

Error Response Examples

Invalid credentials (401)

{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid partner credentials."
  }
}

Decryption failed (400)

{
  "success": false,
  "error": {
    "code": "DECRYPTION_FAILED",
    "message": "Failed to decrypt the request payload. Verify the encryption parameters."
  }
}

Business name taken (409)

{
  "success": false,
  "error": {
    "code": "BUSINESS_NAME_TAKEN",
    "message": "Business name is already taken. Update business name and try again."
  }
}

Validation error (422)

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid business data in encrypted payload.",
    "details": {
      "business_name": ["The business name field is required."],
      "email": ["The email field must be a valid email address."]
    }
  }
}

Partner disabled (503)

{
  "success": false,
  "error": {
    "code": "PARTNER_DISABLED",
    "message": "Partner integration is currently disabled."
  }
}

Subdomain & Business URL

When a new business is created, its subdomain slug is generated from the business name by PartnerRegistrationService::generateAvailableSlug():

Business Name Generated Slug Business URL
Acme Rentals acme-rentals https://acme-rentals.<MAIN_DOMAIN>
My Store my-store https://my-store.<MAIN_DOMAIN>
Acme Rentals (slug taken) acme-rentals-k3x9qa (random) https://acme-rentals-k3x9qa.<MAIN_DOMAIN>

Slug rules:

  • Base = Str::slug(business_name), cut to 56 characters, with leading/trailing hyphens trimmed
  • The base is used as-is when it is not empty, not a reserved subdomain (config('domains.reserved_subdomains'): app, api, accounts, docs, www, mail, admin), and not taken by any business, including soft-deleted ones
  • Otherwise a hyphen and 6 random lowercase letters/digits are appended, trying up to 3 suffixes
  • If the insert still hits the slug unique index, the transaction is retried up to 3 times; after that the request fails with BUSINESS_NAME_TAKEN
  • The business URL uses the scheme of APP_URL and the verified custom domain if one exists (Business::getUrl())

Using the Returned Credentials

After registration, use the credentials to interact with the HeldSway API:

1. Immediate use with access_token

The access_token returned in the registration response is valid for expires_in seconds:

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

2. Long-term use with api_key + api_secret

After the access token expires, issue new tokens using the API key and secret:

# Step 1: Get a new access token
curl -X POST https://api.heldsway.com/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "api_key": "<api_key>",
    "api_secret": "<api_secret>"
  }'

# Step 2: Use the new token
curl https://api.heldsway.com/api/v1/affiliates \
  -H "Authorization: Bearer <new_access_token>"

See Token Issuance for full token issuance details.


Set Password

Set the business owner’s password inline, using the one-time setup token from Register Business (source: App\Http\Controllers\Api\V1\Partner\SetPasswordController → PartnerRegistrationService::setPassword() → AccountSetupService::consume()). The password travels inside the encrypted envelope; the setup token travels in plain JSON.

POST /api/v1/partner/set-password
Authorization: Bearer <PARTNER_TOKEN>
Content-Type: application/json

Auth: partner.auth:rentmy (partner Bearer token, no scope) Rate limit: 10 requests/minute per IP (throttle:10,1)

Request Body

Rules from SetPasswordRequest::rules().

Field Type Required Description
payload string Yes Base64-encoded AES-256-CBC ciphertext of the password JSON (see Encryption Specification)
iv string Yes Base64-encoded 16-byte initialization vector
mac string Yes 64-character hex HMAC-SHA256 digest
setup_token string Yes password_setup.token from the Register Business response

Decrypted Payload Fields

Field Type Required Constraints
password string Yes Password::defaults(): at least 8 characters; in production also mixed case, letters, numbers and symbols (AppServiceProvider::boot())
password_confirmation string Yes Must equal password

Behaviour

  • The setup token is valid only while it is unconsumed and unexpired (password_setup.expires_in seconds after it was issued; account_setup.partner_api_expiry_minutes, default 15 minutes) and only for the partner that connected the business.
  • On success the owner’s password is replaced, their email is marked verified if it was not, the token is consumed, and every other pending setup token for the same owner and business (for example an emailed setup link) is consumed too.

Success Response — 200 OK

{
  "success": true,
  "message": "Password set successfully.",
  "data": { "email": "john@acme.com" }
}

Error Reference

HTTP Status Error Code Cause
400 DECRYPTION_FAILED Bad base64 in payload or iv, wrong IV length, or MAC mismatch
401 UNAUTHORIZED Missing or invalid partner Bearer token
409 ACCOUNT_DEACTIVATED The owner account behind the token is missing or inactive
422 INVALID_SETUP_TOKEN Token unknown, expired, already used, or issued for a business connected by another partner. One message for every case: “This setup link is invalid, expired, or already used.”
422 VALIDATION_ERROR The decrypted password fails the rules; message “Invalid password in encrypted payload.”, per-field messages in details
422 — Missing or malformed envelope fields or setup_token, rejected by SetPasswordRequest with Laravel’s standard validation body (message + errors), no code
429 — More than 10 requests/minute from this IP. Laravel’s throttle response: {"message": "Too Many Attempts."} with a Retry-After header
500 SET_PASSWORD_FAILED Unexpected error that is not a RuntimeException
503 PARTNER_DISABLED Partner integration is disabled
503 PARTNER_NOT_CONFIGURED Partner token is not set on the server

A RuntimeException the mapper does not recognise is re-thrown and returns Laravel’s generic 500 body instead of SET_PASSWORD_FAILED.


Rate Limiting

All three partner endpoints (check-business, register-business, set-password) are throttled at 10 requests per minute per IP address. Exceeding the limit returns HTTP 429 Too Many Requests with a Retry-After header indicating when you can retry.

Business Connection API

Check your business’s connection status and programmatically disconnect from the platform. These endpoints operate on the business associated with the authenticated API token — no business ID parameter is needed.


Authentication

Requires a Bearer access token with the appropriate scope:

Scope Grants Access To
connections:read Retrieve connection status and business details
connections:write Disconnect the business from the platform
Authorization: Bearer <access_token>

To obtain a token, see Token Issuance.


Rate Limits

Scope Limit
connections:read 120 requests/minute
connections:write 60 requests/minute


Data Models

Connection Status Object

The object returned by GET /api/v1/connection/status:

{
  "connected": true,
  "business_details": {
    "name": "Acme Corp",
    "slug": "acme",
    "website": "https://acme.com",
    "subdomain_url": "http://acme.heldsway.com",
    "logo_url": "https://cdn.example.com/logo.png",
    "commission_rate": 15
  },
  "subscription": {
    "status": "active",
    "plan_name": "Growth",
    "interval": "month",
    "price_cents": 4900,
    "trial": null,
    "current_period_end": "2026-11-15T00:00:00.000000Z",
    "auto_renew": true,
    "payment_method_added": true,
    "payment_method": { "brand": "visa", "last4": "4242" },
    "recommended_plan": null
  }
}

Connection Status Field Reference

Field Type Description
connected boolean true when the business is active on the platform. Always true for a valid token (inactive businesses are rejected at the authentication layer).

Business Details Field Reference

Field Type Description
name string The registered business name
slug string Unique URL-safe identifier used in the subdomain ({slug}.heldsway.com)
website string|null The business’s website URL. null if not configured.
subdomain_url string Full URL of the business’s panel. Uses the verified custom domain if available, otherwise falls back to the subdomain pattern.
logo_url string|null URL for the business logo (from branding settings). null if no logo is configured.
commission_rate number|null The business’s default commission rate (%). null if not configured (uses platform default). Per-affiliate overrides take priority.

Get Connection Status

Retrieve the connection status and profile details for the business associated with the current API token.

GET /api/v1/connection/status

Scope: connections:read

Example request:

curl https://api.heldsway.com/api/v1/connection/status \
  -H "Authorization: Bearer <token>"

Response — 200 OK

{
  "success": true,
  "message": "OK",
  "data": {
    "connected": true,
    "business_details": {
      "name": "Acme Corp",
      "slug": "acme",
      "website": "https://acme.com",
      "subdomain_url": "http://acme.heldsway.com",
      "logo_url": "https://cdn.example.com/logo.png",
      "commission_rate": 15
    }
  }
}

Response — 200 OK (no optional fields configured)

{
  "success": true,
  "message": "OK",
  "data": {
    "connected": true,
    "business_details": {
      "name": "Acme Corp",
      "slug": "acme",
      "website": null,
      "subdomain_url": "http://acme.heldsway.com",
      "logo_url": null,
      "commission_rate": null
    }
  }
}

Note: This endpoint will always return connected: true for a valid token. If the business has been disconnected (inactive), the authentication middleware rejects the request before it reaches this endpoint — you will receive a 403 Forbidden response instead.


Disconnect Business

Deactivate the business on the platform. This is a destructive, one-way operation from the API perspective.

POST /api/v1/connection/disconnect
Content-Type: application/json

Scope: connections:write

Body Parameters — All fields are optional.

Field Type Required Constraints Description
reason string|null No Max 500 characters Optional reason for disconnecting. Recorded in the audit log for administrative reference.

Example request:

curl -X POST https://api.heldsway.com/api/v1/connection/disconnect \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Migrating to a different platform" }'

Response — 200 OK

{
  "success": true,
  "message": "Business disconnected successfully."
}

Example — Disconnect without a reason:

curl -X POST https://api.heldsway.com/api/v1/connection/disconnect \
  -H "Authorization: Bearer <token>"

Response — 200 OK

{
  "success": true,
  "message": "Business disconnected successfully."
}

Error — 422 Unprocessable Entity (reason too long — Laravel’s standard validation body from DisconnectBusinessApiRequest)

{
  "message": "The reason field must not be greater than 500 characters.",
  "errors": {
    "reason": ["The reason field must not be greater than 500 characters."]
  }
}

Side Effects

After a successful disconnect:

Effect Detail
Business deactivated businesses.is_active is set to false
All API tokens invalidated The authentication middleware rejects tokens for inactive businesses. All existing tokens for this business immediately stop working.
Audit log entry created A status_changed entry is written to activity_logs with the disconnect reason (if provided), business ID, and timestamp.
Subdomain access blocked The business’s subdomain panel ({slug}.heldsway.com) becomes inaccessible.

Reconnection

Reconnection is not available via the API. Only a platform super admin can reactivate a disconnected business from the main admin panel. Contact your platform administrator to restore access.

Idempotency

Once disconnected, subsequent calls to this endpoint will fail with 403 Forbidden because the token is no longer valid for an inactive business. The endpoint does not need explicit idempotency handling — the authentication layer enforces it naturally.


Error Reference

All endpoints follow the standard response envelope.

HTTP Status Error Code Cause
401 UNAUTHORIZED Missing or invalid Bearer token
402 SUBSCRIPTION_REQUIRED POST /api/v1/connection/disconnect only — the business has no active subscription (see the REST API overview, Error Reference → Subscription Required (402)). GET /api/v1/connection/status is not gated
403 FORBIDDEN Token does not have the required scope (connections:read or connections:write), or the business is already disconnected (inactive)
422 — Invalid request body. DisconnectBusinessApiRequest rejects it with Laravel’s standard validation body (message + errors), no code
429 RATE_LIMIT_EXCEEDED Rate limit exceeded
500 SERVER_ERROR Unexpected server error

Quick Reference

Method Path Scope Description
GET /api/v1/connection/status connections:read Get connection status and business details
POST /api/v1/connection/disconnect connections:write Disconnect business from the platform