Skip to content

Partner Business Registration API

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.

RequirementDetails
AuthenticationAuthorization: Bearer <PARTNER_TOKEN> header
EncryptionRequest body must be AES-256-CBC encrypted using the same PARTNER_TOKEN
Rate Limit10 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

ParameterValue
CipherAES-256-CBC
Key DerivationSHA-256(PARTNER_TOKEN) — produces a 32-byte encryption key
IV16 bytes, randomly generated per request
IntegrityHMAC-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)

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)

javascript
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)

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)

FieldTypeRequiredDescription
payloadstringYesBase64-encoded AES-256-CBC ciphertext
ivstringYesBase64-encoded 16-byte initialization vector
macstringYes64-character hex HMAC-SHA256 digest

Decrypted Payload Fields

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

FieldTypeRequiredConstraintsDescription
business_namestringYesmax 255 charsName of the business
owner_namestringYesmax 255 charsFull name of the business owner
emailstringYesvalid email, max 255 charsOwner's email address
phonestringNomax 50 charsOwner's phone number
addressstringNomax 500 charsBusiness address
website_urlstringNovalid URL, max 255 charsBusiness website

Example

bash
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

json
{
  "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

FieldTypeDescription
business.idintegerUnique business identifier
business.namestringBusiness display name
business.slugstringURL-safe subdomain slug
business.urlstringFull URL for the business panel
credentials.api_keystringPublic API key (prefix: as_). Safe to store.
credentials.api_secretstringSensitive. Shown only once. Store securely.
credentials.access_tokenstringShort-lived Bearer token for immediate API use
credentials.token_typestringAlways "Bearer"
credentials.expires_inintegerAccess token lifetime in seconds (4 hours)
credentials.scopesstring[]Scopes granted to this API key
owner.emailstringOwner's email address
owner.namestringOwner'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](authentication.md).
  • If the owner email already belongs to an existing HeldSway user, the business is created under that existing account.

Error Reference

HTTP StatusError CodeCause
401UNAUTHORIZEDMissing or invalid Authorization: Bearer header
400DECRYPTION_FAILEDPayload could not be decrypted (bad ciphertext, IV, or MAC)
409BUSINESS_EXISTSA business with the same name already exists for this owner
422VALIDATION_ERRORMissing or invalid fields in the decrypted payload
429Rate limit exceeded (10 requests/minute)
503PARTNER_DISABLEDPartner integration is disabled or not configured
503PARTNER_NOT_CONFIGUREDPartner token is not set on the server

Error Response Examples

Invalid credentials (401)

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

Decryption failed (400)

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

Duplicate business (409)

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

Validation error (422)

json
{
  "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)

json
{
  "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 NameGenerated SlugBusiness URL
Acme Rentalsacme-rentalshttp://acme-rentals.<APP_DOMAIN>
My Storemy-storehttp://my-store.<APP_DOMAIN>
Acme Rentals (duplicate slug)acme-rentals-2http://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:

bash
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:

bash
# 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.

Released under the MIT License.