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
- Derive the key:
key = SHA-256(PARTNER_TOKEN)(binary, 32 bytes) - Generate IV: 16 random bytes
- Encrypt:
ciphertext = AES-256-CBC(JSON_payload, key, iv)usingOPENSSL_RAW_DATAflag - Compute MAC:
mac = HMAC-SHA256(iv_bytes + ciphertext_bytes, key)— hex-encoded, 64 characters - Encode: Base64-encode both
ciphertextandiv
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_secretis returned only once in this response. It cannot be retrieved again. Store it securely. - The
access_tokenexpires after 4 hours. Use theapi_keyandapi_secretto 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_namevia 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: truefor a valid token. If the business has been disconnected (inactive), the authentication middleware rejects the request before it reaches this endpoint — you will receive a403 Forbiddenresponse 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 |