Appearance
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.
| 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)
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/jsonRequest 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
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
| 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](authentication.md). - 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)
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 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:
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.