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

This endpoint is available exclusively to authorized partners with a pre-shared PARTNER_TOKEN.

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,
}

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

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 No valid URL, max 255 chars Business website

Example

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

Success Response

HTTP 201 Created

{
  "success": true,
  "message": "Business registered successfully.",
  "data": {
    "business": {
      "id": 42,
      "name": "Acme Rentals",
      "slug": "acme-rentals",
      "url": "http://acme-rentals.example.com"
    },
    "credentials": {
      "api_key": "as_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPqRsTu",
      "api_secret": "aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkL",
      "access_token": "xYzAbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGh",
      "token_type": "Bearer",
      "expires_in": 14400,
      "scopes": [
        "affiliates:read",
        "affiliates:write",
        "conversions:read",
        "conversions:write",
        "clicks:read",
        "clicks:write",
        "commissions:read",
        "reports:read",
        "sdk:init",
        "pageviews:write"
      ]
    },
    "owner": {
      "email": "john@acme.com",
      "name": "John Doe"
    }
  }
}

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 Full URL for the business panel
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 (4 hours)
credentials.scopes string[] Scopes granted to this API key
owner.email string Owner’s email address
owner.name string Owner’s display name

Important Notes

  • The api_secret is returned only once in this response. It cannot be retrieved again. Store it securely.
  • The access_token expires after 4 hours. Use the api_key and api_secret to issue new tokens via [POST /v1/auth/token](/docs/api/authentication).
  • If the owner email already belongs to an existing HeldSway user, the business is created under that existing account.

Error Reference

HTTP Status Error Code Cause
401 UNAUTHORIZED Missing or invalid Authorization: Bearer header
400 DECRYPTION_FAILED Payload could not be decrypted (bad ciphertext, IV, or MAC)
409 BUSINESS_EXISTS A business with the same name already exists for this owner
422 VALIDATION_ERROR Missing or invalid fields in the decrypted payload
429 Rate limit exceeded (10 requests/minute)
503 PARTNER_DISABLED Partner integration is disabled or not configured
503 PARTNER_NOT_CONFIGURED Partner token is not set on the server

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."
  }
}

Duplicate business (409)

{
  "success": false,
  "error": {
    "code": "BUSINESS_EXISTS",
    "message": "Business 'Acme Rentals' already exists for this owner."
  }
}

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 business is registered, a subdomain slug is automatically generated from the business name:

Business Name Generated Slug Business URL
Acme Rentals acme-rentals http://acme-rentals.<APP_DOMAIN>
My Store my-store http://my-store.<APP_DOMAIN>
Acme Rentals (duplicate slug) acme-rentals-2 http://acme-rentals-2.<APP_DOMAIN>

Slug rules:

  • Lowercase alphanumeric characters and hyphens only
  • Auto-generated from business_name via URL-safe transliteration
  • If the slug is already taken, a numeric suffix is appended (-2, -3, etc.)

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 4 hours:

curl https://<API_DOMAIN>/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_DOMAIN>/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_DOMAIN>/api/v1/affiliates \
  -H "Authorization: Bearer <new_access_token>"

See Authentication for full token issuance details.


Rate Limiting

This endpoint is 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 authentication.md.


Rate Limits

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


Data Models

Connection Status Object

The object returned by GET /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
  }
}

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 /v1/connection/status

Scope: connections:read

Example request:

curl https://<API_DOMAIN>/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 /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_DOMAIN>/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_DOMAIN>/api/v1/connection/disconnect \
  -H "Authorization: Bearer <token>"

Response — 200 OK

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

Error — 422 Unprocessable Entity (reason too long)

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": {
      "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
403 FORBIDDEN Token does not have the required scope (connections:read or connections:write), or the business is already disconnected (inactive)
422 VALIDATION_ERROR Invalid request body (see details for field-level errors)
429 Rate limit exceeded
500 SERVER_ERROR Unexpected server error

Quick Reference

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