# Get Affiliate Source: https://chariow.dev/api-reference/affiliates/get-affiliate GET /affiliates/{affiliateCode} Retrieve details of a specific affiliate by their unique code Retrieves detailed information about a specific affiliate by their unique affiliate code. This endpoint returns comprehensive affiliate information including their account details, performance statistics, and earnings. ## Path Parameters The unique affiliate code (e.g., `CREATOR123`, `PARTNER2025`) ## Response Response status message Unique store affiliate identifier (e.g., `saff_abc123xyz`) Affiliate status: `active`, `inactive`, or `suspended` How the affiliate joined the store Human-readable label (e.g., `Invitation`, `Application`) Description of the source Source value: `invitation`, `application`, `manual` Total number of visits through affiliate links Total number of sales made through referrals Total commission earnings Earnings amount Formatted earnings with currency symbol (e.g., `$1,250.00`) Short formatted earnings (e.g., `$1,250`) Currency code (e.g., `USD`) ISO 8601 timestamp of first referral visit ISO 8601 timestamp of most recent referral visit ISO 8601 timestamp when affiliate was suspended (if applicable) Reason for suspension (if applicable) Affiliate account details Affiliate account public ID Affiliate's display name/username Affiliate's country information Account status User details (name, email, etc.) Account creation timestamp ISO 8601 creation timestamp ISO 8601 last update timestamp Array of error messages (empty on success) ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/affiliates/CREATOR123" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript Node.js theme={null} const response = await fetch('https://api.chariow.com/v1/affiliates/CREATOR123', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { data } = await response.json(); console.log(data); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/affiliates/CREATOR123'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_your_api_key' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/affiliates/CREATOR123', headers={'Authorization': 'Bearer sk_live_your_api_key'} ) data = response.json() ``` ```json Response (200) theme={null} { "message": "Affiliate retrieved successfully", "data": { "id": "saff_xyz789abc", "status": "active", "source": { "label": "Invitation", "description": "Joined via invitation", "value": "invitation" }, "total_visits": 150, "total_sales": 25, "total_earnings": { "value": 1250, "formatted": "$1,250.00", "short": "1.25K", "currency": "USD" }, "first_visit_at": "2025-01-16T09:00:00+00:00", "last_visit_at": "2025-01-25T14:30:00+00:00", "suspended_at": null, "suspended_reason": null, "account": { "id": "aff_abc123def", "pseudo": "creative_studio", "country": { "code": "US", "name": "United States" }, "status": "active", "user": { "name": "John Doe", "email": "john@example.com" }, "created_at": "2025-01-15T10:30:00+00:00" }, "created_at": "2025-01-15T10:30:00+00:00", "updated_at": "2025-01-25T14:30:00+00:00" }, "errors": [] } ``` ```json Not Found (404) theme={null} { "message": "Affiliate not found", "data": [], "errors": [] } ``` ```json Unauthorised (401) theme={null} { "message": "Unauthenticated", "data": [], "errors": [] } ``` # Send Affiliate Invitations Source: https://chariow.dev/api-reference/affiliates/send-invitations POST /affiliates/invitations Send invitation emails to potential affiliates Sends invitation emails to the provided email addresses. Up to 25 emails can be sent in a single request. Existing affiliates and pending invitations are automatically skipped. ## Request Body Array of email addresses (1-25 items) **Example:** `["john@example.com", "jane@example.com"]` ## Response Array of created invitation objects Unique invitation identifier (e.g., `affinv_abc123xyz`) Email address the invitation was sent to Invitation status: `pending` ISO 8601 expiration timestamp ISO 8601 creation timestamp Details of skipped emails Emails that are already registered as affiliates Emails that have pending invitations ```bash cURL theme={null} curl -X POST "https://api.chariow.com/v1/affiliates/invitations" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "emails": ["john@example.com", "jane@example.com"] }' ``` ```javascript Node.js theme={null} const response = await fetch('https://api.chariow.com/v1/affiliates/invitations', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ emails: ['john@example.com', 'jane@example.com'] }) }); const data = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/affiliates/invitations'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'emails' => ['john@example.com', 'jane@example.com'] ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); ``` ```json Response (201 Created) theme={null} { "message": "2 invitations sent successfully", "data": { "invitations": [ { "id": "affinv_abc123xyz", "email": "john@example.com", "status": "pending", "expires_at": "2026-02-10T10:30:00+00:00", "created_at": "2026-01-11T10:30:00+00:00" }, { "id": "affinv_def456uvw", "email": "jane@example.com", "status": "pending", "expires_at": "2026-02-10T10:30:00+00:00", "created_at": "2026-01-11T10:30:00+00:00" } ], "skipped": { "already_affiliate": [], "already_invited": [] } }, "errors": [] } ``` ```json Partial Success (201 Created) theme={null} { "message": "1 invitation sent successfully", "data": { "invitations": [ { "id": "affinv_abc123xyz", "email": "john@example.com", "status": "pending", "expires_at": "2026-02-10T10:30:00+00:00", "created_at": "2026-01-11T10:30:00+00:00" } ], "skipped": { "already_affiliate": ["existing@affiliate.com"], "already_invited": ["pending@invitation.com"] } }, "errors": [] } ``` ```json Validation Error (422) theme={null} { "message": "The emails field is required.", "data": [], "errors": { "emails": ["The emails field is required."] } } ``` # Authentication Source: https://chariow.dev/api-reference/authentication How to authenticate with the Chariow API The Chariow API uses API keys to authenticate requests. ## Creating an API Key Go to [app.chariow.com](https://app.chariow.com) and log in to your account. Click on **Settings** in the sidebar. Select **API Keys** from the settings menu. Click **Create API Key**, give it a descriptive name, and copy the generated key. Copy your API key immediately after creation. For security reasons, the full key is only shown once. ## Making Authenticated Requests Include your API key in the `Authorization` header: ```bash theme={null} curl -X GET "https://api.chariow.com/v1/store" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Security Best Practices Never expose your API key in client-side code or public repositories. * Store keys in environment variables * Use different keys for development and production * Rotate keys periodically * Monitor key usage in your dashboard ## Authentication Errors | Status | Message | Cause | | ------ | -------------------- | -------------------------------- | | 401 | `API key is missing` | Missing Authorization header | | 401 | `Invalid API key` | Key doesn't exist or was revoked | # Initiate Checkout Source: https://chariow.dev/api-reference/checkout/init-checkout POST /checkout Create a new checkout session for a product purchase Initiates a new checkout session for purchasing a product. Creates a sale record and either returns a payment checkout URL for paid products or completes the sale immediately for free products. **Unsupported product types** β€” The following cannot be used to initiate a checkout through the API: * **Service** products * **Coaching** products * Products with **pay-what-you-want** pricing For these, redirect customers to your [Chariow storefront](https://chariow.com) or use the **Snap Widget** embed on your website. All sales initiated via this API endpoint will have their **Channel** set to **"API"** on your store dashboard. This helps you identify and track sales originating from your API integrations separately from other channels. ## Repeat Purchases The ability to purchase a product multiple times depends on the product type: | Product Type | Repeat Purchase | Behaviour | | ---------------- | --------------- | --------------------------------------------------------------------------------------------------------- | | **License** | Always allowed | Customers can purchase license products multiple times. Each purchase generates a new unique license key. | | **Downloadable** | Blocked | Returns `already_purchased` if customer has an active access grant. | | **Course** | Blocked | Returns `already_purchased` if customer has an active access grant. | | **Bundle** | Blocked | Returns `already_purchased` if customer has an active access grant. | For blocked product types, if a customer's access has been **revoked** (e.g., after a refund), they will be able to purchase the product again. The system checks for **active** access grants only. ## Authentication This endpoint requires API key authentication via Bearer token: ``` Authorization: Bearer sk_live_your_api_key ``` ## Request Body The product public ID or slug to purchase. Example: `prd_abc123xyz` or `premium-course` Customer email address. Must be a valid email (max 255 characters). Example: `customer@example.com` Customer first name (max 50 characters). Example: `John` Customer last name (max 50 characters). Example: `Doe` Customer phone details Phone number (numeric only). Example: `1234567890` ISO country code (max 10 characters). Example: `US`, `FR`, `GB` Discount code to apply (max 100 characters). Example: `SAVE20` Campaign public ID or tracking code for analytics. Example: `camp_xyz789` Custom field values for the product (key-value pairs). Must match the product's configured custom fields. Currency code for payment (ISO 4217). Defaults to store currency if not provided. Example: `USD`, `EUR`, `GBP` Custom URL to redirect customers after payment completion (max 2048 characters). When provided, customers will be redirected to this URL instead of the default Chariow post-purchase page. Must be a valid active URL. Example: `https://yoursite.com/thank-you` Custom key-value metadata to store with the sale. Maximum 10 keys allowed, each value limited to 255 characters. This metadata is included in Pulse webhook payloads, making it useful for linking sales with external systems. Example: `{"order_ref": "ORD-123", "source": "landing_page"}` The buyer's IP address, IPv4 or IPv6. Example: `203.0.113.42` **Why send `customer_ip`?** This endpoint is called from your server, so the IP we see is your own infrastructure, not the buyer's. When you forward `customer_ip`, we store it on the sale and resolve the buyer's country from it, which improves the payment methods offered at checkout and the accuracy of your sales analytics. When the field is omitted, nothing breaks: we simply fall back to the calling IP. ### Shipping Address Fields The following fields are **required** when the product has "Require shipping address" enabled. If shipping is not required for the product, these fields are ignored. Customer street address for shipping (max 255 characters). Example: `123 Main Street` Customer city for shipping (max 100 characters). Example: `New York` Customer state or region for shipping (max 100 characters). Example: `NY` Customer country for shipping (ISO 3166-1 alpha-2 code, max 2 characters). Example: `US` Customer postal/ZIP code for shipping (max 20 characters). Example: `10001` ## Response The checkout response object containing step, purchase, and payment information Current checkout step. Possible values: * `payment`: Payment required, redirect to checkout\_url * `completed`: Sale completed (free products) * `already_purchased`: Customer already owns this product Optional message for the customer (e.g., "You have already purchased this product") Sale information (null if already\_purchased) Sale public ID. Example: `sal_abc123xyz` Sale status. Values: `awaiting_payment`, `completed`, `failed`, `refunded` Original product price before discounts Amount value Human-readable formatted amount Short formatted amount ISO 4217 currency code Final amount after discounts Total discount applied Payment details Payment amount (may differ from sale amount due to currency conversion) Payment status Exchange rate if multi-currency Store information Product information Customer information Applied discount details (if any) Product access details (files, licenses, course content, etc.) Payment information Payment checkout URL. Redirect customers to this URL to complete payment. Null for free products. Payment transaction ID. Null for free products. ## Error Responses Invalid or missing API key Product not found or not published Validation errors, pay-what-you-want product, or unsupported product type (Service, Coaching) ```bash cURL theme={null} curl -X POST "https://api.chariow.com/v1/checkout" \ -H "Authorization: Bearer sk_live_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "product_id": "prd_abc123xyz", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "discount_code": "SAVE20", "redirect_url": "https://yoursite.com/thank-you", "customer_ip": "203.0.113.42", "custom_metadata": { "order_ref": "ORD-123", "source": "landing_page" } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: 'prd_abc123xyz', email: 'customer@example.com', first_name: 'John', last_name: 'Doe', phone: { number: '1234567890', country_code: 'US' }, discount_code: 'SAVE20', redirect_url: 'https://yoursite.com/thank-you', customer_ip: '203.0.113.42', custom_metadata: { order_ref: 'ORD-123', source: 'landing_page' } }) }); const result = await response.json(); if (result.data.step === 'payment') { // Redirect to payment URL window.location.href = result.data.payment.checkout_url; } else if (result.data.step === 'completed') { // Free product - sale completed console.log('Purchase completed:', result.data.purchase.id); } ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/checkout'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_your_api_key', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'product_id' => 'prd_abc123xyz', 'email' => 'customer@example.com', 'first_name' => 'John', 'last_name' => 'Doe', 'phone' => [ 'number' => '1234567890', 'country_code' => 'US' ], 'discount_code' => 'SAVE20', 'redirect_url' => 'https://yoursite.com/thank-you', 'customer_ip' => '203.0.113.42', 'custom_metadata' => [ 'order_ref' => 'ORD-123', 'source' => 'landing_page' ] ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); if ($data['data']['step'] === 'payment') { // Redirect to payment URL header('Location: ' . $data['data']['payment']['checkout_url']); } elseif ($data['data']['step'] === 'completed') { // Free product - sale completed echo 'Purchase completed: ' . $data['data']['purchase']['id']; } ``` ```python Python theme={null} import requests response = requests.post( 'https://api.chariow.com/v1/checkout', headers={ 'Authorization': 'Bearer sk_live_your_api_key', 'Content-Type': 'application/json' }, json={ 'product_id': 'prd_abc123xyz', 'email': 'customer@example.com', 'first_name': 'John', 'last_name': 'Doe', 'phone': { 'number': '1234567890', 'country_code': 'US' }, 'discount_code': 'SAVE20', 'redirect_url': 'https://yoursite.com/thank-you', 'customer_ip': '203.0.113.42', 'custom_metadata': { 'order_ref': 'ORD-123', 'source': 'landing_page' } } ) data = response.json() if data['data']['step'] == 'payment': # Redirect to payment URL checkout_url = data['data']['payment']['checkout_url'] print(f'Redirect to: {checkout_url}') elif data['data']['step'] == 'completed': # Free product - sale completed print(f"Purchase completed: {data['data']['purchase']['id']}") ``` ```json Paid Product - Awaiting Payment theme={null} { "data": { "step": "payment", "message": null, "purchase": { "id": "sal_abc123xyz", "status": "awaiting_payment", "original_amount": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "discount_amount": { "value": 19.80, "formatted": "$19.80", "short": "20", "currency": "USD" }, "payment": { "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "status": "awaiting_payment", "exchange_rate": { "value": 1, "formatted": "$1.00", "short": "1", "currency": "USD" } }, "store": { "id": "str_xyz789", "name": "My Store" }, "product": { "id": "prd_abc123xyz", "name": "Premium Course", "slug": "premium-course" }, "customer": { "id": "cus_def456", "email": "customer@example.com", "first_name": "John", "last_name": "Doe" }, "discount": { "id": "dis_ghi789", "code": "SAVE20", "type": "percentage", "value": 20 }, "post_purchase": { "files": [], "licenses": [], "courses": [] } }, "payment": { "checkout_url": "https://payment.example.com/checkout?token=abc123", "transaction_id": "txn_xyz789abc" } } } ``` ```json Free Product - Completed theme={null} { "data": { "step": "completed", "message": null, "purchase": { "id": "sal_abc123xyz", "status": "completed", "original_amount": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "amount": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "discount_amount": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "payment": { "amount": null, "status": "completed", "exchange_rate": { "value": 1, "formatted": "$1.00", "short": "1", "currency": "USD" } }, "store": { "id": "str_xyz789", "name": "My Store" }, "product": { "id": "prd_abc123xyz", "name": "Free eBook", "slug": "free-ebook" }, "customer": { "id": "cus_def456", "email": "customer@example.com", "first_name": "John", "last_name": "Doe" }, "discount": null, "post_purchase": { "files": [ { "id": "fil_jkl012", "name": "ebook.pdf", "download_url": "https://files.chariow.com/download/..." } ], "licenses": [], "courses": [] } }, "payment": { "checkout_url": null, "transaction_id": null } } } ``` ```json Error - Product Not Found (404) theme={null} { "message": "Product not found", "data": [], "errors": [] } ``` ```json Error - Pay-What-You-Want Product (422) theme={null} { "message": "Pay-what-you-want products are not supported via the Public API. Please redirect customers to your Chariow store or use the Snap Widget embed on your website.", "data": [], "errors": [] } ``` ```json Error - Unsupported Product Type (422) theme={null} { "message": "Service and Coaching products are not supported via the Public API. Please redirect customers to your Chariow store or use the Snap Widget embed on your website.", "data": [], "errors": [] } ``` ```json Error - Validation Failed (422) theme={null} { "message": "The email field must be a valid email address.", "data": [], "errors": { "email": [ "The email field must be a valid email address." ], "phone.number": [ "The phone.number field is required." ] } } ``` ```json Error - Invalid Discount Code (422) theme={null} { "message": "The discount code is invalid or has expired.", "data": [], "errors": { "discount_code": [ "The discount code is invalid or has expired." ] } } ``` ```json Error - Missing Shipping Address (422) theme={null} { "message": "Please enter your address for shipping. (and 4 more errors)", "data": [], "errors": { "address": [ "Please enter your address for shipping." ], "city": [ "Please enter your city for shipping." ], "state": [ "Please enter your state/region for shipping." ], "country": [ "Please enter your country for shipping." ], "zip": [ "Please enter your postal code for shipping." ] } } ``` # Get Customer Source: https://chariow.dev/api-reference/customers/get-customer GET /customers/{customerId} Retrieve details of a specific customer Retrieves detailed information about a specific customer by their public ID. Returns the complete customer profile including contact information and store details. ## Path Parameters The unique customer identifier (e.g., `cus_abc123xyz`) ## Response Unique customer identifier (e.g., `cus_abc123xyz`) Customer full name (combination of first and last name) Customer first name Customer last name Customer email address URL to the customer's avatar image Customer phone number International formatted phone number (e.g., `+1 234 567 890`) Country information Country name ISO 3166-1 alpha-2 country code ISO 3166-1 alpha-3 country code Country dial code Country currency code Country flag emoji Store information Store public ID Store name Store logo URL Store URL ISO 8601 timestamp of when the customer was created ISO 8601 timestamp of when the customer was last updated ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/customers/cus_abc123xyz" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/customers/cus_abc123xyz', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { data } = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/customers/cus_abc123xyz'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_your_api_key' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/customers/cus_abc123xyz', headers={'Authorization': 'Bearer sk_live_your_api_key'} ) data = response.json()['data'] ``` ```json Success (200) theme={null} { "message": "success", "data": { "id": "cus_abc123xyz", "name": "John Doe", "first_name": "John", "last_name": "Doe", "email": "john@example.com", "avatar_url": "https://cdn.chariow.com/avatars/abc123.jpg", "phone": { "number": "+1 234 567 890", "country": { "name": "United States", "code": "US", "alpha_3_code": "USA", "dial_code": "+1", "currency": "USD", "flag": "πŸ‡ΊπŸ‡Έ" } }, "store": { "id": "str_xyz789", "name": "My Digital Store", "logo_url": "https://cdn.chariow.com/stores/xyz789/logo.png", "url": "https://mystore.chariow.link" }, "created_at": "2025-01-15T10:30:00+00:00", "updated_at": "2025-01-20T14:45:00+00:00" }, "errors": [] } ``` ```json Not Found (404) theme={null} { "message": "Customer not found", "data": [], "errors": [] } ``` # List Customers Source: https://chariow.dev/api-reference/customers/list-customers GET /customers Retrieve all customers from your store Retrieves a cursor-paginated list of all customers in your store. Customers are created automatically when they make a purchase. This endpoint supports search filtering and date range filtering. ## Query Parameters Number of customers to return per page (max 100) Cursor for pagination. Use the `next_cursor` from the previous response. Search customers by name, email, or phone number Filter customers created from this date (Y-m-d format, e.g., `2025-01-01`) Filter customers created until this date (Y-m-d format, e.g., `2025-01-31`) ## Response Array of customer objects Unique customer identifier (e.g., `cus_abc123xyz`) Customer full name (combination of first and last name) Customer first name Customer last name Customer email address URL to the customer's avatar image Customer phone number International formatted phone number (e.g., `+1 234 567 890`) Country information Country name ISO 3166-1 alpha-2 country code ISO 3166-1 alpha-3 country code Country dial code Country currency code Country flag emoji Store information Store public ID Store name Store logo URL Store URL ISO 8601 timestamp of when the customer was created ISO 8601 timestamp of when the customer was last updated Cursor pagination metadata Cursor for the next page (null if no more pages) Cursor for the previous page (null if on first page) Whether there are more pages available ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/customers?per_page=20&search=john" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/customers?per_page=20&search=john', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { data } = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/customers?per_page=20&search=john'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_your_api_key' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/customers', params={'per_page': 20, 'search': 'john'}, headers={'Authorization': 'Bearer sk_live_your_api_key'} ) data = response.json()['data'] ``` ```json Response theme={null} { "message": "success", "data": { "data": [ { "id": "cus_abc123xyz", "name": "John Doe", "first_name": "John", "last_name": "Doe", "email": "john@example.com", "avatar_url": "https://cdn.chariow.com/avatars/abc123.jpg", "phone": { "number": "+1 234 567 890", "country": { "name": "United States", "code": "US", "alpha_3_code": "USA", "dial_code": "+1", "currency": "USD", "flag": "πŸ‡ΊπŸ‡Έ" } }, "store": { "id": "str_xyz789", "name": "My Digital Store", "logo_url": "https://cdn.chariow.com/stores/xyz789/logo.png", "url": "https://mystore.chariow.link" }, "created_at": "2025-01-15T10:30:00+00:00", "updated_at": "2025-01-20T14:45:00+00:00" } ], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true } }, "errors": [] } ``` # Get Discount Source: https://chariow.dev/api-reference/discounts/get-discount GET /discounts/{discountPublicId} Retrieve details of a specific discount code Retrieves detailed information about a specific discount code by its public ID, including type, value, usage statistics, validity period, and associated products. ## Path Parameters The unique discount identifier (e.g., `dis_abc123`) ## Response Unique discount identifier (e.g., `dis_abc123`) Display name of the discount The discount code customers use at checkout Discount type (`percentage` or `fixed`) Discount status (`active` or `expired`) Discount value information Raw discount value (percentage number or amount in currency units) Human-readable formatted value (e.g., `20%` or `$10.00`) Array of products this discount applies to (empty if applies to all products) Store information (simplified) Email of customer this discount is restricted to (null if no restriction) Maximum number of uses (null if unlimited) Number of times the discount has been used ISO 8601 timestamp when discount becomes active (null if no start restriction) ISO 8601 timestamp when discount expires (null if no expiration) Whether the discount was automatically generated by the system ISO 8601 timestamp when discount was created ISO 8601 timestamp when discount was last updated ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/discounts/dis_abc123" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/discounts/dis_abc123', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { message, data } = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/discounts/dis_abc123'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_your_api_key' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $result = json_decode($response, true); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/discounts/dis_abc123', headers={'Authorization': 'Bearer sk_live_your_api_key'} ) discount = response.json()['data'] ``` ```json Response theme={null} { "message": "success", "data": { "id": "dis_abc123", "name": "Summer Sale", "code": "SUMMER20", "type": "percentage", "status": "active", "value_off": { "raw": 20, "formatted": "20%" }, "products": [ { "id": "prd_def456", "name": "Premium Course", "type": "course", "pictures": { "thumbnail": "https://cdn.chariow.com/thumb.jpg", "cover": "https://cdn.chariow.com/cover.jpg" }, "category": { "value": "education", "label": "Education" }, "pricing": { "type": "one_time", "price": { "value": 9999, "formatted": "$99.99", "short": "100", "currency": "USD" } }, "bundle": null } ], "store": { "id": "str_xyz789", "name": "My Store" }, "customer_email": null, "usage_limit": 100, "usage_count": 15, "start_date": "2025-01-01T00:00:00+00:00", "end_date": "2025-12-31T23:59:59+00:00", "is_auto_generated": false, "created_at": "2025-01-01T00:00:00+00:00", "updated_at": "2025-01-15T10:30:00+00:00" }, "errors": [] } ``` ```json Not Found (404) theme={null} { "message": "No query results for model [App\\Models\\Discount].", "data": [], "errors": [] } ``` # List Discounts Source: https://chariow.dev/api-reference/discounts/list-discounts GET /discounts Retrieve all discount codes from your store Retrieves a cursor-paginated list of all discount codes in your store with optional filtering and search capabilities. ## Query Parameters Number of discounts to return per page (max 100) Cursor for pagination. Use the `next_cursor` from the previous response. Filter by status (`active`, `expired`) Search by discount code, name, or public ID Filter discounts created from this date (Y-m-d format, e.g., `2025-01-01`) Filter discounts created until this date (Y-m-d format, e.g., `2025-01-31`) ## Response Array of discount objects Unique discount identifier (e.g., `dis_abc123`) Display name of the discount The discount code customers use at checkout Discount type (`percentage` or `fixed`) Discount status (`active` or `expired`) Discount value information Raw discount value (percentage number or amount in currency units) Human-readable formatted value (e.g., `20%` or `$10.00`) Array of products this discount applies to (empty if applies to all products) Store information (simplified) Email of customer this discount is restricted to (null if no restriction) Maximum number of uses (null if unlimited) Number of times the discount has been used ISO 8601 timestamp when discount becomes active (null if no start restriction) ISO 8601 timestamp when discount expires (null if no expiration) Whether the discount was automatically generated by the system ISO 8601 timestamp when discount was created ISO 8601 timestamp when discount was last updated Cursor pagination metadata Cursor for the next page (null if no more pages) Cursor for the previous page (null if on first page) Whether there are more results available ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/discounts?status=active&per_page=20" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/discounts?status=active&per_page=20', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { message, data } = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/discounts?status=active&per_page=20'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_your_api_key' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $result = json_decode($response, true); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/discounts', params={'status': 'active', 'per_page': 20}, headers={'Authorization': 'Bearer sk_live_your_api_key'} ) data = response.json()['data'] ``` ```json Response theme={null} { "message": "success", "data": { "data": [ { "id": "dis_abc123xyz", "name": "Summer Sale", "code": "SUMMER20", "type": "percentage", "status": "active", "value_off": { "raw": 20, "formatted": "20%" }, "products": [ { "id": "prd_def456", "name": "Premium Course", "type": "course", "pictures": { "thumbnail": "https://cdn.chariow.com/thumb.jpg", "cover": "https://cdn.chariow.com/cover.jpg" }, "category": { "value": "education", "label": "Education" }, "pricing": { "type": "one_time", "price": { "value": 9999, "formatted": "$99.99", "short": "100", "currency": "USD" } }, "bundle": null } ], "store": { "id": "str_xyz789", "name": "My Store" }, "customer_email": null, "usage_limit": 100, "usage_count": 15, "start_date": "2025-01-01T00:00:00+00:00", "end_date": "2025-12-31T23:59:59+00:00", "is_auto_generated": false, "created_at": "2025-01-01T00:00:00+00:00", "updated_at": "2025-01-15T10:30:00+00:00" }, { "id": "dis_def456abc", "name": "VIP Discount", "code": "VIP50", "type": "fixed", "status": "active", "value_off": { "raw": 50, "formatted": "$50.00" }, "products": [], "store": { "id": "str_xyz789", "name": "My Store" }, "customer_email": "vip@example.com", "usage_limit": 1, "usage_count": 0, "start_date": null, "end_date": null, "is_auto_generated": false, "created_at": "2025-01-10T08:00:00+00:00", "updated_at": "2025-01-10T08:00:00+00:00" } ], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true } }, "errors": [] } ``` # Errors Source: https://chariow.dev/api-reference/errors API error codes and handling The Chariow API uses conventional HTTP response codes to indicate success or failure. ## HTTP Status Codes | Code | Description | | ---- | ---------------- | | 200 | Success | | 201 | Created | | 400 | Bad Request | | 401 | Unauthorised | | 403 | Forbidden | | 404 | Not Found | | 422 | Validation Error | | 429 | Rate Limited | | 500 | Server Error | ## Error Response Format ```json theme={null} { "message": "Error description", "data": [], "errors": { "field_name": ["Validation error message"] } } ``` ## Common Errors ### Validation Error (422) ```json theme={null} { "message": "The given data was invalid.", "data": [], "errors": { "email": ["The email field is required."], "product_id": ["The selected product_id is invalid."] } } ``` ### Not Found (404) ```json theme={null} { "message": "No query results for model [App\\Models\\Product].", "data": [], "errors": [] } ``` ### Rate Limited (429) ```json theme={null} { "message": "Rate limit exceeded. Please retry after 60 seconds.", "data": [], "errors": [] } ``` # API Introduction Source: https://chariow.dev/api-reference/introduction Welcome to the Chariow API Reference The Chariow API is a RESTful API that allows you to programmatically interact with your store. You can use it to retrieve products, manage customers, process sales, validate licenses, and more. ## Base URL All API requests should be made to: ``` https://api.chariow.com/v1 ``` ## Authentication The API uses Bearer token authentication. Include your API key in the `Authorization` header: ```bash theme={null} Authorization: Bearer sk_live_your_api_key ``` Generate API keys in your store dashboard ## Request Format * All requests should include `Content-Type: application/json` header for POST/PUT requests * Request bodies should be JSON encoded * Query parameters should be URL encoded ## Response Format All responses follow a consistent JSON structure: ```json theme={null} { "message": "success", "data": { // Response data }, "errors": [] } ``` ### Pagination List endpoints use cursor-based pagination: ```json theme={null} { "data": { "data": [...], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true } } } ``` Use the `cursor` query parameter to navigate pages: ``` GET /v1/products?cursor=eyJpZCI6NTB9&per_page=20 ``` ## Available Endpoints Retrieve store information List and retrieve products Initiate checkout sessions Manage sales and orders Customer management License key management Discount code management Webhook notification pulses ## Rate Limits | Endpoint Type | Limit | | ---------------- | ------- | | All API requests | 100/min | Rate limits are applied per API key. See [Rate Limits](/en/resources/rate-limits) for more details. ## Need Help? Step-by-step tutorials Get help from our team Join the community # Activate License Source: https://chariow.dev/api-reference/licenses/activate-license POST /licenses/{licenseKey}/activate Activate a license on a device Activates a license for a device by recording activation details and incrementing the activation count. The system automatically captures the requesting IP address and user agent. On the first activation, the license status changes from `pending_activation` to `active`, and the expiration date is calculated based on the product's validity period settings. Each activation is tracked individually, allowing you to view the complete activation history. The license cannot be activated if it has been revoked, has expired, or if the maximum activation limit has been reached. ## Path Parameters The license key to activate (e.g., `ABC-123-XYZ-789`) ## Request Body Optional unique identifier for the device (e.g., MAC address, hardware UUID, machine ID). Max 255 characters. The IP address and user agent are automatically captured from the request and do not need to be provided. ## Response Unique license identifier The license key string License status (will be `active` after successful activation) ISO 8601 timestamp when first activated ISO 8601 expiration timestamp (calculated on first activation) Current number of activations (incremented) Maximum number of activations allowed Number of activations remaining Whether the license is currently active Whether the license can be activated again Product information ## Error Responses Returned when: * License has been revoked * License has expired * Activation limit has been reached Returned when the license key doesn't exist or doesn't belong to your store ```bash cURL theme={null} curl -X POST "https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/activate" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "device_identifier": "00:1B:44:11:3A:B7" }' ``` ```javascript Node.js theme={null} const response = await fetch( 'https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/activate', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ device_identifier: '00:1B:44:11:3A:B7' }) } ); const data = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/activate'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'device_identifier' => '00:1B:44:11:3A:B7' ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); ``` ```json Success (200) theme={null} { "message": "License activated successfully", "data": { "id": "lic_abc123", "sale_id": 156, "customer_id": 89, "license_key": "ABC-123-XYZ-789", "status": "active", "activated_at": "2025-01-15T10:30:00.000000Z", "expires_at": "2026-01-15T10:30:00.000000Z", "expired_at": null, "revoked_at": null, "activation_count": 1, "max_activations": 10, "activations_remaining": 9, "is_active": true, "is_expired": false, "can_activate": true, "metadata": null, "created_at": "2025-01-15T09:00:00.000000Z", "updated_at": "2025-01-15T10:30:00.000000Z", "product": { "id": 42, "name": "Premium Software License" } }, "errors": [] } ``` ```json License Revoked (400) theme={null} { "message": "License has been revoked", "data": [], "errors": [] } ``` ```json Activation Limit Reached (400) theme={null} { "message": "Activation limit reached", "data": [], "errors": [] } ``` ```json License Expired (400) theme={null} { "message": "License has expired", "data": [], "errors": [] } ``` ```json License Not Found (404) theme={null} { "message": "No query results for model [App\\Models\\IssuedLicense].", "data": [], "errors": [] } ``` ## Implementation Notes ### First Activation When a license is activated for the first time: * Status changes from `pending_activation` to `active` * `activated_at` is set to the current timestamp * `expires_at` is calculated based on the product's validity period (if configured) ### Subsequent Activations * `activation_count` is incremented * A new activation record is created in the activation history * The license status remains `active` ### Device Tracking Each activation is tracked with: * `device_identifier` (optional, provided by you) * `activated_by_ip` (automatically captured) * `user_agent` (automatically captured) * `created_at` (timestamp of activation) You can retrieve the full activation history using the [Get License Activations](/api-reference/licenses/get-activations) endpoint. ## Example: Desktop Application ```javascript theme={null} class LicenseActivator { constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = 'https://api.chariow.com/v1'; } async activate(licenseKey) { // Get unique device identifier const deviceId = this.getDeviceIdentifier(); try { const response = await fetch( `${this.baseUrl}/licenses/${licenseKey}/activate`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ device_identifier: deviceId }) } ); if (!response.ok) { const error = await response.json(); throw new Error(error.message); } const result = await response.json(); console.log('License activated successfully'); console.log(`Activations remaining: ${result.data.activations_remaining}`); return result.data; } catch (error) { console.error('Activation failed:', error.message); throw error; } } getDeviceIdentifier() { // Implementation depends on your platform // Examples: MAC address, hardware UUID, machine ID return 'unique-device-id-here'; } } // Usage const activator = new LicenseActivator('sk_live_your_api_key'); await activator.activate('ABC-123-XYZ-789'); ``` # Get License Activations Source: https://chariow.dev/api-reference/licenses/get-activations GET /licenses/{licenseKey}/activations Retrieve activation history for a license Fetches a cursor-paginated list of all activation records for a specific license, ordered by most recent first. Each activation record contains detailed information about when and where the license was activated, including IP address, user agent, device identifier, and timestamp. This endpoint is useful for auditing license usage, tracking device activations, and debugging activation-related issues. The response includes pagination metadata and a summary of total, maximum, and remaining activations. ## Path Parameters The license key to retrieve activations for (e.g., `ABC-123-XYZ-789`) ## Query Parameters Number of activation records to return per page (max 100) Cursor for pagination. Use the `next_cursor` from the previous response. ## Response Array of activation records Unique activation record ID The license ID this activation belongs to IP address from which the activation was made User agent string from the activation request Device identifier provided during activation (if any) Additional metadata attached to the activation ISO 8601 timestamp when the activation was created Cursor pagination metadata Cursor for the next page (null if no more pages) Cursor for the previous page (null if on first page) Whether there are more results Number of items per page Activation summary statistics Total number of activations for this license Maximum number of activations allowed Number of activations remaining ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/activations?per_page=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch( 'https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/activations?per_page=20', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const data = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/activations?per_page=20'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer YOUR_API_KEY' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); ``` ```json Response theme={null} { "message": "success", "data": { "activations": [ { "id": 3, "issued_license_id": 1, "activated_by_ip": "203.0.113.45", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "device_identifier": "00:1B:44:11:3A:B7", "metadata": null, "created_at": "2025-01-18T15:42:00.000000Z" }, { "id": 2, "issued_license_id": 1, "activated_by_ip": "198.51.100.12", "user_agent": "MyApp/1.2.0 (macOS 14.2)", "device_identifier": "A4:5E:60:D8:2F:11", "metadata": null, "created_at": "2025-01-16T09:15:00.000000Z" }, { "id": 1, "issued_license_id": 1, "activated_by_ip": "192.0.2.100", "user_agent": "MyApp/1.0.0 (Windows 11)", "device_identifier": "desktop-workstation-001", "metadata": null, "created_at": "2025-01-15T10:30:00.000000Z" } ], "pagination": { "next_cursor": null, "prev_cursor": null, "has_more": false, "per_page": 20 }, "summary": { "total_activations": 3, "max_activations": 10, "activations_remaining": 7 } }, "errors": [] } ``` ```json License Not Found (404) theme={null} { "message": "No query results for model [App\\Models\\IssuedLicense].", "data": [], "errors": [] } ``` ## Use Cases ### Audit Trail Track all devices that have activated a license: ```javascript theme={null} async function getActivationHistory(licenseKey) { const response = await fetch( `https://api.chariow.com/v1/licenses/${licenseKey}/activations`, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const { data } = await response.json(); console.log(`License activated ${data.summary.total_activations} times`); console.log(`${data.summary.activations_remaining} activations remaining`); data.activations.forEach(activation => { console.log(`Device: ${activation.device_identifier}`); console.log(`IP: ${activation.activated_by_ip}`); console.log(`Date: ${activation.created_at}`); }); return data; } ``` ### Device Management Display activation history to customers: ```javascript theme={null} async function showCustomerActivations(licenseKey) { const response = await fetch( `https://api.chariow.com/v1/licenses/${licenseKey}/activations`, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const { data } = await response.json(); return { devices: data.activations.map(a => ({ name: a.device_identifier || 'Unknown Device', ip: a.activated_by_ip, activatedAt: new Date(a.created_at).toLocaleDateString() })), summary: data.summary }; } ``` ### Fraud Detection Identify suspicious activation patterns: ```javascript theme={null} async function detectSuspiciousActivity(licenseKey) { const response = await fetch( `https://api.chariow.com/v1/licenses/${licenseKey}/activations`, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const { data } = await response.json(); // Check for multiple activations from different locations const uniqueIPs = new Set(data.activations.map(a => a.activated_by_ip)); if (uniqueIPs.size > 5) { console.warn('Suspicious: License activated from multiple IP addresses'); } // Check for rapid activations const activations = data.activations.map(a => new Date(a.created_at)); // ... implement your logic return { suspicious: uniqueIPs.size > 5, uniqueLocations: uniqueIPs.size, activations: data.activations }; } ``` ## Pagination This endpoint uses cursor-based pagination for efficient traversal of large activation lists: ```javascript theme={null} async function getAllActivations(licenseKey) { let allActivations = []; let cursor = null; do { const url = cursor ? `https://api.chariow.com/v1/licenses/${licenseKey}/activations?cursor=${cursor}` : `https://api.chariow.com/v1/licenses/${licenseKey}/activations`; const response = await fetch(url, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const { data } = await response.json(); allActivations = [...allActivations, ...data.activations]; cursor = data.pagination.next_cursor; } while (cursor); return allActivations; } ``` ## Notes * Activations are ordered by most recent first (`created_at DESC`) * Each activation is immutable and cannot be modified * Activation records persist even after a license is revoked * The `device_identifier` field is optional and may be `null` if not provided during activation * IP addresses and user agents are automatically captured during activation # Get License Source: https://chariow.dev/api-reference/licenses/get-license GET /licenses/{licenseKey} Retrieve details of a specific license Retrieves detailed information about a specific license by its license key. This endpoint returns comprehensive license information including activation status, usage counts, expiration dates, and associated product data. ## Path Parameters The license key (e.g., `ABC-123-XYZ-789`) ## Response Unique license identifier (e.g., `lic_abc123`) Associated sale ID Associated customer ID The license key string License status: `pending_activation`, `active`, `expired`, or `revoked` ISO 8601 timestamp when first activated (null if not yet activated) ISO 8601 expiration timestamp (null if no expiration) ISO 8601 timestamp when expired (null if not expired) ISO 8601 timestamp when revoked (null if not revoked) Current number of activations Maximum number of activations allowed Number of activations remaining Whether the license is currently active Whether the license has expired Whether the license can be activated (has remaining activations) Custom metadata attached to the license ISO 8601 creation timestamp ISO 8601 last update timestamp Product information Product ID Product name ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/licenses/ABC-123-XYZ-789" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch('https://api.chariow.com/v1/licenses/ABC-123-XYZ-789', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/licenses/ABC-123-XYZ-789'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer YOUR_API_KEY' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); ``` ```json Response theme={null} { "message": "success", "data": { "id": "lic_ghi789", "sale_id": 156, "customer_id": 89, "license_key": "ABC-123-XYZ-789", "status": "active", "activated_at": "2025-01-15T10:30:00.000000Z", "expires_at": "2026-01-15T10:30:00.000000Z", "expired_at": null, "revoked_at": null, "activation_count": 3, "max_activations": 10, "activations_remaining": 7, "is_active": true, "is_expired": false, "can_activate": true, "metadata": null, "created_at": "2025-01-15T09:00:00.000000Z", "updated_at": "2025-01-15T10:30:00.000000Z", "product": { "id": 42, "name": "Premium Software License" } }, "errors": [] } ``` ```json Not Found (404) theme={null} { "message": "No query results for model [App\\Models\\IssuedLicense].", "data": [], "errors": [] } ``` # List Licenses Source: https://chariow.dev/api-reference/licenses/list-licenses GET /licenses Retrieve all licenses from your store Retrieves a cursor-paginated list of all licenses in your store. Licenses are automatically generated when customers purchase license-based products. This endpoint supports filtering by status, customer, and product. ## Query Parameters Number of licenses to return per page (max 100) Cursor for pagination. Use the `next_cursor` from the previous response. Filter by license status. Valid values: `pending_activation`, `active`, `expired`, `revoked` Filter by customer public ID (e.g., `cus_abc123`) Filter by product public ID (e.g., `prd_def456`) ## Response Array of license objects Unique license identifier (e.g., `lic_abc123`) Associated sale ID Associated customer ID The license key string License status: `pending_activation`, `active`, `expired`, or `revoked` ISO 8601 timestamp when first activated (null if not yet activated) ISO 8601 expiration timestamp (null if no expiration) ISO 8601 timestamp when expired (null if not expired) ISO 8601 timestamp when revoked (null if not revoked) Current number of activations Maximum number of activations allowed Number of activations remaining Whether the license is currently active Whether the license has expired Whether the license can be activated (has remaining activations) Custom metadata attached to the license ISO 8601 creation timestamp ISO 8601 last update timestamp Product information Product ID Product name Cursor pagination metadata Cursor for the next page (null if no more pages) Cursor for the previous page (null if on first page) Whether there are more results Number of items per page ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/licenses?status=active&per_page=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch('https://api.chariow.com/v1/licenses?status=active&per_page=20', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/licenses?status=active&per_page=20'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer YOUR_API_KEY' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); ``` ```json Response theme={null} { "message": "success", "data": { "data": [ { "id": "lic_ghi789", "sale_id": 156, "customer_id": 89, "license_key": "ABC-123-XYZ-789", "status": "active", "activated_at": "2025-01-15T10:30:00.000000Z", "expires_at": "2026-01-15T10:30:00.000000Z", "expired_at": null, "revoked_at": null, "activation_count": 3, "max_activations": 10, "activations_remaining": 7, "is_active": true, "is_expired": false, "can_activate": true, "metadata": null, "created_at": "2025-01-15T09:00:00.000000Z", "updated_at": "2025-01-15T10:30:00.000000Z", "product": { "id": 42, "name": "Premium Software License" } } ], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true, "per_page": 20 } }, "errors": [] } ``` # Revoke License Source: https://chariow.dev/api-reference/licenses/revoke-license POST /licenses/{licenseKey}/revoke Permanently revoke a license Permanently revokes a license by changing its status to `revoked` and recording the revocation timestamp. Once revoked, the license cannot be activated on any device, and this action cannot be undone. The license will remain visible in reports and history for audit purposes. This endpoint is useful for handling customer refunds, policy violations, or when a license needs to be immediately terminated for any reason. An optional reason parameter allows you to document why the license was revoked. Revoking a license is **permanent and irreversible**. The license cannot be reactivated after revocation. ## Path Parameters The license key to revoke (e.g., `ABC-123-XYZ-789`) ## Request Body Optional reason for revocation. Max 500 characters. Useful for audit trails and customer support. ## Response Unique license identifier The license key string License status (will be `revoked`) ISO 8601 timestamp when the license was revoked Number of activations before revocation Maximum number of activations (for reference) Will show remaining count, but license cannot be activated Will be `false` after revocation Will be `false` after revocation Product information ## Error Responses Returned when the license has already been revoked Returned when the license key doesn't exist or doesn't belong to your store Returned when validation fails (e.g., reason exceeds 500 characters) ```bash cURL theme={null} curl -X POST "https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/revoke" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reason": "Customer requested refund" }' ``` ```javascript Node.js theme={null} const response = await fetch( 'https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/revoke', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'Customer requested refund' }) } ); const data = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/revoke'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'reason' => 'Customer requested refund' ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); ``` ```json Success (200) theme={null} { "message": "License revoked successfully", "data": { "id": "lic_abc123", "status": "revoked", "customer": { "id": "cus_xyz789", "name": "John Doe", "email": "john@example.com" }, "product": { "id": "prd_abc456", "name": "Premium Software License", "slug": "premium-software-license" }, "license": { "key": "ABC-123-XYZ-789", "masked_key": "ABC-***-***-789" }, "is_active": false, "is_expired": false, "can_activate": false, "activations": { "count": 3, "max": 10, "remaining": 7 }, "certificate_url": null, "metadata": null, "activated_at": "2025-01-15T10:30:00.000000Z", "expires_at": "2026-01-15T10:30:00.000000Z", "expired_at": null, "revoked_at": "2025-01-20T14:22:00.000000Z", "created_at": "2025-01-15T09:00:00.000000Z", "updated_at": "2025-01-20T14:22:00.000000Z" }, "errors": [] } ``` ```json Already Revoked (400) theme={null} { "message": "License has already been revoked", "data": [], "errors": [] } ``` ```json Validation Error (422) theme={null} { "message": "The reason must not exceed 500 characters.", "data": [], "errors": { "reason": ["The reason must not exceed 500 characters."] } } ``` ```json License Not Found (404) theme={null} { "message": "No query results for model [App\\Models\\IssuedLicense].", "data": [], "errors": [] } ``` ## Common Use Cases ### Refund Processing When processing a refund, revoke the license to prevent further use: ```javascript theme={null} async function processRefund(licenseKey) { await fetch(`https://api.chariow.com/v1/licenses/${licenseKey}/revoke`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'Full refund processed via payment gateway' }) }); } ``` ### Terms of Service Violation Revoke licenses that violate your terms: ```javascript theme={null} async function revokeViolation(licenseKey, violationType) { await fetch(`https://api.chariow.com/v1/licenses/${licenseKey}/revoke`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: `Terms of service violation: ${violationType}` }) }); } ``` ### Fraudulent Purchase Immediately revoke licenses from suspected fraudulent purchases: ```javascript theme={null} async function revokeFraudulent(licenseKey) { await fetch(`https://api.chariow.com/v1/licenses/${licenseKey}/revoke`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'Suspected fraudulent purchase - payment flagged by gateway' }) }); } ``` ## What Happens After Revocation Once a license is revoked: 1. **Status Change**: The license status becomes `revoked` 2. **Timestamp**: The `revoked_at` field is set to the current timestamp 3. **No Future Activations**: The license cannot be activated on any device 4. **Existing Activations**: Any existing activations remain in the system for audit purposes 5. **Permanent**: The revocation **cannot be undone** - you must issue a new license if needed 6. **Visible in Reports**: The license remains visible in your dashboard and API responses for historical tracking ## Alternative: Deactivating Individual Devices If you want to free up an activation slot without permanently revoking the entire license, you should instead remove specific activations. This can be done through your store dashboard, allowing the license to be activated on a different device. There is currently no Public API endpoint for deactivating individual device activations. This must be done through the store dashboard or Core API. # Get Product Source: https://chariow.dev/api-reference/products/get-product GET /products/{productId} Retrieve detailed information about a specific product Retrieves comprehensive information about a specific product by its public ID or slug. This endpoint returns detailed pricing information, images, ratings, sales count, SEO data, custom fields, and bundle information (if applicable). ## Path Parameters The unique product identifier or slug (e.g., `prd_abc123` or `premium-course`) ## Response Response status message Unique product identifier (e.g., `prd_abc123`) Product name URL-friendly product identifier Full product description Product type: `downloadable`, `service`, `course`, `license`, `bundle`, or `coaching` Category value Human-readable category label Product status (always `published` for public API) Whether the product is free Thumbnail image URL Cover image URL Pricing type: `free`, `one_time`, or `what_you_want` Current price (reflects sale price if on sale) Price amount as decimal Formatted price string Short formatted price string Currency code Base price (same structure as current\_price) Sale price if product is on sale (same structure as current\_price) Minimum price for "pay what you want" products Suggested price for "pay what you want" products Discount percentage (e.g., "34%") Stock quantity information (only for products with limited quantity) Total available quantity Remaining quantity Remaining percentage Sold quantity Sold percentage Total quantity Whether shipping address is required Average rating (0-5) Number of ratings Sale end date (ISO 8601 format) Number of successful sales (null if hidden) SEO settings (only when loaded) SEO title SEO description SEO keywords Custom call-to-action text CTA value CTA label Custom fields (only when loaded) Bundle information (only for bundle products) Total bundle value Savings amount Savings percentage Array of error messages (empty on success) ```bash cURL (by ID) theme={null} curl -X GET "https://api.chariow.com/v1/products/prd_abc123" \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` ```bash cURL (by slug) theme={null} curl -X GET "https://api.chariow.com/v1/products/premium-course" \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch('https://api.chariow.com/v1/products/prd_abc123', { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' } }); const { data } = await response.json(); console.log(data); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/products/prd_abc123'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_YOUR_API_KEY' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/products/prd_abc123', headers={'Authorization': 'Bearer sk_live_YOUR_API_KEY'} ) data = response.json() ``` ```json Success Response (200) theme={null} { "message": "success", "data": { "id": "prd_abc123", "name": "Premium Web Development Course", "slug": "premium-web-dev-course", "description": "A comprehensive course covering advanced web development techniques and best practices.", "type": "course", "category": { "value": "education_and_learning", "label": "Education and Learning" }, "status": "published", "is_free": false, "pictures": { "thumbnail": "https://cdn.chariow.com/products/abc123/thumb.jpg", "cover": "https://cdn.chariow.com/products/abc123/cover.jpg" }, "pricing": { "type": "one_time", "current_price": { "value": 99.00, "formatted": "$99.00", "short": "99", "currency": "USD" }, "price": { "value": 149.00, "formatted": "$149.00", "short": "149", "currency": "USD" }, "sale_price": { "value": 99.00, "formatted": "$99.00", "short": "99", "currency": "USD" }, "min_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "suggested_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "price_off": "34%" }, "quantity": null, "settings": { "is_requires_shipping_address": false }, "rating": { "average": 4.8, "count": 245 }, "on_sale_until": "2025-02-28T23:59:59Z", "sales_count": 1250, "seo": null, "custom_cta_text": { "value": null, "label": null }, "fields": null, "bundle": null }, "errors": [] } ``` ```json Bundle Product Response theme={null} { "message": "success", "data": { "id": "prd_bundle789", "name": "Complete Developer Bundle", "slug": "developer-bundle", "description": "Everything you need to become a professional developer.", "type": "bundle", "category": { "value": "technology", "label": "Technology" }, "status": "published", "is_free": false, "pictures": { "thumbnail": "https://cdn.chariow.com/products/bundle789/thumb.jpg", "cover": null }, "pricing": { "type": "one_time", "current_price": { "value": 199.00, "formatted": "$199.00", "short": "199", "currency": "USD" }, "price": { "value": 199.00, "formatted": "$199.00", "short": "199", "currency": "USD" }, "sale_price": null, "min_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "suggested_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "price_off": null }, "quantity": null, "settings": { "is_requires_shipping_address": false }, "rating": { "average": 4.9, "count": 89 }, "on_sale_until": null, "sales_count": 543, "seo": null, "custom_cta_text": { "value": null, "label": null }, "fields": null, "bundle": { "value": { "value": 297.00, "formatted": "$297.00", "short": "297", "currency": "USD" }, "savings": { "amount": { "value": 98.00, "formatted": "$98.00", "short": "98", "currency": "USD" }, "percentage": "33%" } } }, "errors": [] } ``` ```json Not Found (404) theme={null} { "message": "Product not found", "data": [], "errors": [] } ``` ```json Unauthorised (401) theme={null} { "message": "Unauthorised", "data": [], "errors": ["Invalid API key"] } ``` # List Products Source: https://chariow.dev/api-reference/products/list-products GET /products Retrieve all published products from your store Retrieves a cursor-paginated list of all published products in your store. Only products in published status are returned. Supports optional filtering by category, type, or search term. ## Query Parameters Number of products to return per page (maximum 100) Cursor for pagination. Use the `next_cursor` from the previous response to fetch the next page. Search products by name or slug Filter by product category. Available values: `creative_arts`, `technology`, `business_and_finance`, `personal_development`, `education_and_learning`, `entertainment`, `health_and_wellness`, `literature_and_publishing`, `media_and_communication`, `miscellaneous` Filter by product type. Available values: `downloadable`, `service`, `course`, `license`, `bundle`, `coaching` ## Response Response status message Array of product objects Unique product identifier (e.g., `prd_abc123`) Product name URL-friendly product identifier Product type: `downloadable`, `service`, `course`, `license`, `bundle`, or `coaching` Category value Human-readable category label Product status (always `published` for public API) Whether the product is free Thumbnail image URL Cover image URL Pricing type: `free`, `one_time`, or `what_you_want` Price amount as decimal (e.g., 99.00) Formatted price string (e.g., `$99.00`) Short formatted price string (e.g., `$99`) Currency code (e.g., `USD`) Base price (same structure as current\_price) Cursor for the next page Cursor for the previous page Whether more results are available Array of error messages (empty on success) ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/products?per_page=20&type=course" \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch('https://api.chariow.com/v1/products?per_page=20&type=course', { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' } }); const { data } = await response.json(); console.log(data); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/products?per_page=20&type=course'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_YOUR_API_KEY' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/products', params={'per_page': 20, 'type': 'course'}, headers={'Authorization': 'Bearer sk_live_YOUR_API_KEY'} ) data = response.json() ``` ```json Success Response (200) theme={null} { "message": "success", "data": { "data": [ { "id": "prd_abc123", "name": "Premium Course", "slug": "premium-course", "type": "course", "category": { "value": "education_and_learning", "label": "Education and Learning" }, "status": "published", "is_free": false, "pictures": { "thumbnail": "https://cdn.chariow.com/products/abc123/thumb.jpg", "cover": "https://cdn.chariow.com/products/abc123/cover.jpg" }, "pricing": { "type": "one_time", "current_price": { "value": 99.00, "formatted": "$99.00", "short": "99", "currency": "USD" }, "price": { "value": 99.00, "formatted": "$99.00", "short": "99", "currency": "USD" } } }, { "id": "prd_def456", "name": "Pro Software License", "slug": "pro-software", "type": "license", "category": { "value": "technology", "label": "Technology" }, "status": "published", "is_free": false, "pictures": { "thumbnail": null, "cover": null }, "pricing": { "type": "one_time", "current_price": { "value": 49.00, "formatted": "$49.00", "short": "49", "currency": "USD" }, "price": { "value": 49.00, "formatted": "$49.00", "short": "49", "currency": "USD" } } } ], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true } }, "errors": [] } ``` ```json Unauthorised (401) theme={null} { "message": "Unauthorised", "data": [], "errors": ["Invalid API key"] } ``` # Get Pulse Source: https://chariow.dev/api-reference/pulses/get-pulse GET /pulses/{pulsePublicId} Retrieve details of a specific webhook notification pulse Retrieves detailed information about a specific pulse webhook by its public ID, including configured triggers, associated products, source information, and current status. ## Path Parameters The unique pulse identifier (e.g., `pulse_abc123`) ## Response Unique pulse identifier (e.g., `pulse_abc123xyz`) Webhook URL where notifications are sent Whether the pulse is currently active Source information about how the pulse was created Source value: `manual`, `zapier`, `make`, or `system` Human-readable source label Description of the source Array of trigger event objects that will activate this pulse Trigger event value (e.g., `successful_sale`, `license_activated`) Human-readable label Description of when this trigger fires Array of products this pulse is limited to. Empty array means all products will trigger this pulse. Product identifier Product name Product slug Store information Store identifier Store name Whether the pulse can be deleted. Only manually created pulses can be deleted. ISO 8601 timestamp with timezone ISO 8601 timestamp with timezone ## Available Trigger Events Pulses can be configured to trigger on the following events: ### Sale Events | Value | Label | Description | | ----------------- | --------------- | ----------------------------------- | | `successful_sale` | Successful Sale | Triggers when a sale is successful. | | `abandoned_sale` | Abandoned Sale | Triggers when a sale is abandoned. | | `failed_sale` | Failed Sale | Triggers when a sale fails. | ### License Events | Value | Label | Description | | ------------------------ | ---------------------- | ------------------------------------------------------- | | `license_activated` | License Activated | Triggers when a license is activated. | | `license_expired` | License Expired | Triggers when a license expires. | | `license_issued` | License Issued | Triggers when a license is issued to a customer. | | `license_nearing_expiry` | License Nearing Expiry | Triggers when a license is approaching its expiry date. | | `license_revoked` | License Revoked | Triggers when a license is revoked. | ### Special Events | Value | Label | Description | | ----- | ---------- | ------------------------ | | `all` | All Events | Triggers for all events. | ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/pulses/pulse_abc123xyz" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch('https://api.chariow.com/v1/pulses/pulse_abc123xyz', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/pulses/pulse_abc123xyz'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer YOUR_API_KEY' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); ``` ```json Response theme={null} { "message": "success", "data": { "id": "pulse_abc123xyz", "url": "https://example.com/webhooks/chariow", "is_enabled": true, "source": { "value": "manual", "label": "Manual", "description": "Created manually by user" }, "triggers": [ { "value": "successful_sale", "label": "Successful Sale", "description": "Triggers when a sale is successful." }, { "value": "license_activated", "label": "License Activated", "description": "Triggers when a license is activated." } ], "products": [ { "id": "prd_xyz789", "name": "Premium Course", "slug": "premium-course" } ], "store": { "id": "str_abc123", "name": "My Digital Store" }, "can_delete": true, "created_at": "2025-01-15T10:30:00+00:00", "updated_at": "2025-01-15T10:30:00+00:00" }, "errors": [] } ``` ```json Not Found (404) theme={null} { "message": "Pulse not found", "data": [], "errors": [] } ``` # List Pulses Source: https://chariow.dev/api-reference/pulses/list-pulses GET /pulses Retrieve all webhook notification pulses for your store Retrieves a cursor-paginated list of all pulse webhooks configured for your store. Pulses are webhook notifications sent to your specified endpoints when specific events occur, such as successful sales, abandoned sales, or license activations. ## Query Parameters Number of pulses to return per page (maximum 100) Cursor for pagination. Use the `next_cursor` from the previous response to retrieve the next page. Search pulses by URL, public ID, or trigger event names ## Response Array of pulse objects Unique pulse identifier (e.g., `pulse_abc123xyz`) Webhook URL where notifications are sent Whether the pulse is currently active Source information about how the pulse was created Source value: `manual`, `zapier`, `make`, or `system` Human-readable source label Description of the source Array of trigger event objects that will activate this pulse Trigger event value (e.g., `successful_sale`, `license_activated`) Human-readable label Description of when this trigger fires Array of products this pulse is limited to. Empty array means all products will trigger this pulse. Product identifier Product name Product slug Store information Store identifier Store name Whether the pulse can be deleted. System-created pulses cannot be deleted. ISO 8601 timestamp with timezone ISO 8601 timestamp with timezone Cursor pagination metadata Cursor for the next page, or null if no more pages Cursor for the previous page, or null if on first page Whether there are more items to fetch ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/pulses?per_page=20&search=webhook" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch('https://api.chariow.com/v1/pulses?per_page=20&search=webhook', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/pulses?per_page=20&search=webhook'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer YOUR_API_KEY' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); ``` ```json Response theme={null} { "message": "success", "data": { "data": [ { "id": "pulse_abc123xyz", "url": "https://example.com/webhooks/chariow", "is_enabled": true, "source": { "value": "manual", "label": "Manual", "description": "Created manually by user" }, "triggers": [ { "value": "successful_sale", "label": "Successful Sale", "description": "Triggers when a sale is successful." }, { "value": "license_activated", "label": "License Activated", "description": "Triggers when a license is activated." } ], "products": [], "store": { "id": "str_xyz789", "name": "My Digital Store" }, "can_delete": true, "created_at": "2025-01-15T10:30:00+00:00", "updated_at": "2025-01-15T10:30:00+00:00" }, { "id": "pulse_def456abc", "url": "https://hooks.zapier.com/hooks/catch/12345/abcde", "is_enabled": true, "source": { "value": "zapier", "label": "Zapier", "description": "Created via Zapier integration" }, "triggers": [ { "value": "successful_sale", "label": "Successful Sale", "description": "Triggers when a sale is successful." } ], "products": [ { "id": "prd_xyz789", "name": "Premium Course", "slug": "premium-course" } ], "store": { "id": "str_xyz789", "name": "My Digital Store" }, "can_delete": false, "created_at": "2025-01-10T08:00:00+00:00", "updated_at": "2025-01-10T08:00:00+00:00" } ], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true } }, "errors": [] } ``` # Get Sale Source: https://chariow.dev/api-reference/sales/get-sale GET /sales/{saleId} Retrieve details of a specific sale Retrieves comprehensive information about a specific sale by its public ID, including customer details, product information, payment details, applied discounts, marketing campaign, shipping information, and customer rating. ## Path Parameters The unique sale public identifier (e.g., `sal_abc123xyz`) ## Response Unique sale identifier (e.g., `sal_abc123xyz`) Sale status (`awaiting_payment`, `completed`, `failed`, `abandoned`, `settled`) Sales channel information Channel value (`store`, `affiliate`, `discover`, `widget`, `api`) Human-readable label Channel description Final amount charged Amount value (e.g., 99 for \$99.00) Formatted amount with currency symbol Short formatted amount Three-letter ISO currency code Original amount before discount Discount amount applied Settlement details Amount to be settled to merchant ISO 8601 timestamp when settlement is due ISO 8601 timestamp when settlement was completed Platform service fee amount Download statistics Total number of downloads ISO 8601 timestamp of last download Payment details Payment status (`initiated`, `pending`, `cancelled`, `failed`, `success`) Payment gateway transaction ID Payment gateway used Payment method details (card, mobile money, etc.) Amount paid in payment currency Payment processing fee Payment fee rate as percentage Interchange markup details Exchange rate if multi-currency Payment failure error details (if failed) Shipping address (if provided) Street address City name State or province Country details with code and name Postal/ZIP code Purchase context information Browser user agent Customer IP address Country detected from IP Device type (`desktop`, `mobile`, `tablet`) Customer locale Custom field values provided during checkout Marketing campaign (if tracked) Customer rating (if provided) Store information Product information Customer information Applied discount (if any) Whether sale has been reconciled ISO 8601 timestamp of last reconciliation ISO 8601 timestamp when sale failed ISO 8601 timestamp when sale entered awaiting payment status ISO 8601 timestamp when sale was abandoned ISO 8601 timestamp when sale was completed ISO 8601 timestamp when sale was created ISO 8601 timestamp when sale was last updated ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/sales/sal_abc123xyz" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/sales/sal_abc123xyz', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { data } = await response.json(); console.log(data); // Sale object ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/sales/sal_abc123xyz'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_your_api_key' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/sales/sal_abc123xyz', headers={'Authorization': 'Bearer sk_live_your_api_key'} ) sale = response.json()['data'] ``` ```json Response theme={null} { "message": "success", "data": { "id": "sal_abc123xyz", "status": "completed", "channel": { "value": "store", "label": "Store", "description": "Sale made through the store checkout" }, "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "original_amount": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "discount_amount": { "value": 19.80, "formatted": "$19.80", "short": "20", "currency": "USD" }, "settlement": { "amount": { "value": 75.24, "formatted": "$75.24", "short": "75", "currency": "USD" }, "due_at": "2025-02-01T00:00:00+00:00", "done_at": "2025-02-01T10:15:00+00:00", "service_fee": { "value": 3.96, "formatted": "$3.96", "short": "4", "currency": "USD" } }, "download": { "total": 3, "last_at": "2025-01-20T14:30:00+00:00" }, "payment": { "status": "success", "transaction_id": "txn_moneroo_xyz789", "gateway": "moneroo", "method": { "id": "card", "name": "Credit/Debit Card", "type": "card" }, "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "fee": { "value": 2.37, "formatted": "$2.37", "short": "2", "currency": "USD" }, "fee_rate": "3.00%", "interchange": { "rate": "0.50%", "fee": { "value": 0.40, "formatted": "$0.40", "short": "0", "currency": "USD" } }, "exchange_rate": { "value": 1.0, "formatted": "$1.00", "short": "1", "currency": "USD" }, "failure_error": null }, "shipping": { "address": "123 Main Street", "city": "New York", "state": "NY", "country": { "code": "US", "name": "United States" }, "zip": "10001" }, "context": { "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "ip_address": "203.0.113.42", "country": { "code": "US", "name": "United States" }, "device_type": "desktop", "locale": "en_US" }, "custom_fields_values": { "company_name": "Acme Corp", "vat_number": "GB123456789" }, "campaign": { "id": "cmp_def456", "name": "Black Friday Campaign", "tracking_code": "BF2025" }, "rating": { "id": "rat_ghi789", "value": 5, "comment": "Excellent product!", "created_at": "2025-01-18T09:00:00+00:00" }, "store": { "id": "str_xyz789", "name": "My Digital Store", "slug": "my-digital-store" }, "product": { "id": "prd_jkl012", "name": "Advanced Laravel Course", "slug": "advanced-laravel-course", "type": "course" }, "customer": { "id": "cus_mno345", "email": "customer@example.com", "name": "John Doe", "first_name": "John", "last_name": "Doe" }, "discount": { "id": "dis_pqr678", "code": "SAVE20", "type": "percentage", "value": 20 }, "is_reconciled": true, "last_reconciled_at": "2025-01-16T08:00:00+00:00", "failed_at": null, "awaiting_payment_at": "2025-01-15T10:30:00+00:00", "abandoned_at": null, "completed_at": "2025-01-15T10:32:00+00:00", "created_at": "2025-01-15T10:30:00+00:00", "updated_at": "2025-01-15T10:32:00+00:00" }, "errors": [] } ``` ```json Not Found (404) theme={null} { "message": "Sale not found", "data": [], "errors": [] } ``` # List Sales Source: https://chariow.dev/api-reference/sales/list-sales GET /sales Retrieve all sales from your store Retrieves a cursor-paginated list of all sales in your store. Sales represent completed, pending, abandoned, failed, or settled transactions. The response includes customer, product, discount, and rating information. ## Query Parameters Number of sales to return per page (max 100) Cursor for pagination. Use the `next_cursor` from the previous response. Filter by sale status: `awaiting_payment`, `completed`, `failed`, `abandoned`, or `settled` Filter by customer public ID (e.g., `cus_abc123xyz`) Search by sale reference or customer email Filter sales from this date onwards (format: `Y-m-d`, e.g., `2025-01-01`) Filter sales until this date (format: `Y-m-d`, e.g., `2025-01-31`) ## Response Array of sale objects Unique sale identifier (e.g., `sal_abc123xyz`) Sale status (`awaiting_payment`, `completed`, `failed`, `abandoned`, `settled`) Original amount before discount Amount value (e.g., 99 for \$99.00) Formatted amount with currency symbol (e.g., `$99.00`) Short formatted amount (e.g., `$99`) Three-letter ISO currency code (e.g., `USD`, `EUR`) Final amount charged after discount Discount amount applied Payment details Amount paid in payment currency Payment status (`initiated`, `pending`, `cancelled`, `failed`, `success`) Exchange rate if multi-currency Shipping address details (if provided) Street address City name State or province Country code Postal/ZIP code Store information Product information Customer information Applied discount (if any) Customer rating (if provided) Post-purchase access data (files, licences, instructions) Pagination metadata Cursor for next page (null if no more pages) Cursor for previous page (null if first page) Whether more results are available ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/sales?status=completed&per_page=20&start_date=2025-01-01" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/sales?status=completed&per_page=20&start_date=2025-01-01', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const result = await response.json(); console.log(result.data); // Array of sales console.log(result.pagination); // Pagination info ``` ```php PHP theme={null} $ch = curl_init('https://api.chariow.com/v1/sales?status=completed&per_page=20&start_date=2025-01-01'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_your_api_key' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/sales', params={ 'status': 'completed', 'per_page': 20, 'start_date': '2025-01-01' }, headers={'Authorization': 'Bearer sk_live_your_api_key'} ) data = response.json() sales = data['data'] ``` ```json Response theme={null} { "message": "success", "data": [ { "id": "sal_abc123xyz", "status": "completed", "original_amount": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "discount_amount": { "value": 19.80, "formatted": "$19.80", "short": "20", "currency": "USD" }, "payment": { "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "status": "success", "exchange_rate": { "value": 1.0, "formatted": "$1.00", "short": "1", "currency": "USD" } }, "shipping": { "address": "123 Main Street", "city": "New York", "state": "NY", "country": "US", "zip": "10001" }, "store": { "id": "str_xyz789", "name": "My Store", "slug": "my-store" }, "product": { "id": "prd_def456", "name": "Premium Course", "type": "course" }, "customer": { "id": "cus_ghi789", "email": "customer@example.com", "name": "John Doe" }, "discount": { "id": "dis_jkl012", "code": "SAVE20", "type": "percentage" }, "rate": null, "post_purchase": { "files": [ { "id": "fil_mno345", "name": "course-materials.zip", "size": 15728640, "download_url": "https://cdn.chariow.com/downloads/..." } ], "licences": [], "instructions": "Thank you for your purchase!" } } ], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true }, "errors": [] } ``` # Get Store Source: https://chariow.dev/api-reference/store/get-store GET /store Retrieve detailed information about your store Retrieves comprehensive information about the store associated with the authenticated API key, including branding, social links, status, and appearance settings. ## Authentication This endpoint requires authentication using a Store API key. Include your API key in the `Authorization` header as a Bearer token. ``` Authorization: Bearer YOUR_API_KEY ``` ## Response Status message indicating success or failure Store information object Unique store identifier with `str_` prefix (e.g., `str_abc123xyz`) Store name Store description displayed on your storefront Full URL to the store logo image. Returns `null` if no logo is set. Public URL of the store (custom domain or Chariow subdomain) Social media links configured for the store Telegram channel or group URL Instagram profile URL Facebook page URL X (formerly Twitter) profile URL LinkedIn profile or company page URL YouTube channel URL TikTok profile URL Discord server invite URL Current store status. Possible values: `active`, `suspended`, `pending_review` Store appearance and theme settings (only included when appearance settings are loaded) Selected theme configuration Theme identifier value Human-readable theme name Font configuration for the store Primary font settings Secondary font settings Border styling configuration Default product ordering preference Colour scheme configuration Primary brand colour in HEX and RGB formats Contrast colour for text readability Whether to display featured products section Whether to show purchase button on product cards Whether to display recommended products Number of products displayed per row in grid layout Call-to-action button animation settings Array of error messages (empty on success) ## Error Responses Returned when the API key is missing or invalid ```json theme={null} { "message": "API key is missing. Please provide a valid API key. Help: https://docs.chariow.com", "data": [], "errors": [] } ``` Or: ```json theme={null} { "message": "Invalid API key. Please check again. Help: https://docs.chariow.com", "data": [], "errors": [] } ``` ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/store" \ -H "Authorization: Bearer sk_live_abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/store', { method: 'GET', headers: { 'Authorization': 'Bearer sk_live_abc123xyz...', 'Content-Type': 'application/json' } }); const data = await response.json(); console.log(data); ``` ```php PHP theme={null} ```json Success Response (200) theme={null} { "message": "success", "data": { "id": "str_abc123xyz", "name": "My Digital Store", "description": "A marketplace for premium digital products and courses", "logo_url": "https://cdn.chariow.com/stores/str_abc123xyz/logo.png", "url": "https://mystore.chariow.com", "social_links": { "telegram": null, "instagram": "https://instagram.com/mystore", "facebook": "https://facebook.com/mystore", "x": "https://x.com/mystore", "linkedin": null, "youtube": null, "tiktok": null, "discord": null }, "status": "active", "appearance": { "theme": { "value": "modern", "label": "Modern" }, "font": { "primary": { "value": "inter", "display_name": "Inter", "category": "sans-serif", "url": "https://fonts.googleapis.com/css2?family=Inter" }, "secondary": { "value": "roboto", "display_name": "Roboto", "category": "sans-serif", "url": "https://fonts.googleapis.com/css2?family=Roboto" } }, "border_style": { "value": "rounded", "label": "Rounded" }, "product_order": { "value": "newest", "label": "Newest First" }, "color": { "primary": { "hex": "#3B82F6", "rgb": "59, 130, 246" }, "contrast": { "hex": "#FFFFFF", "rgb": "255, 255, 255" } }, "show_featured_products": true, "show_purchase_button_on_product_card": true, "show_recommended_products": true, "products_per_row": 3, "cta_animation_type": { "value": "pulse", "label": "Pulse" } } }, "errors": [] } ``` ```json Unauthenticated Response (401) theme={null} { "message": "Invalid API key. Please check again. Help: https://docs.chariow.com", "data": [], "errors": [] } ``` # Affiliates Source: https://chariow.dev/en/guides/affiliates Learn how to manage your affiliate programme via the Chariow API Chariow provides a comprehensive affiliate management system that allows you to grow your sales through partnerships. Affiliates can promote your products using unique referral codes and earn commissions on successful sales. ## Affiliate Object An affiliate contains information about their account, performance metrics, and nested account details: ```json theme={null} { "id": "saff_xyz789abc", "status": "active", "source": { "value": "invitation", "label": "Invitation", "description": "Joined via store invitation" }, "total_visits": 156, "total_sales": 32, "total_earnings": { "value": 1250, "formatted": "$1,250.00", "short": "1.25K", "currency": "USD" }, "first_visit_at": "2025-01-16T08:00:00+00:00", "last_visit_at": "2025-02-01T14:22:00+00:00", "suspended_at": null, "suspended_reason": null, "account": { "id": "aff_abc123def", "pseudo": "creative_studio", "country": { "code": "US", "name": "United States" }, "status": "active", "user": { "id": "usr_def456", "name": "John Doe", "email": "john@example.com", "first_name": "John", "last_name": "Doe" }, "created_at": "2025-01-15T09:00:00+00:00" }, "store": { "id": "str_xyz789", "name": "My Digital Store" }, "created_at": "2025-01-15T10:00:00+00:00", "updated_at": "2025-02-01T14:22:00+00:00" } ``` ### Key Fields * **`id`**: Store affiliate public ID (prefixed with `saff_`) * **`status`**: Current status (`active` or `suspended`) * **`source`**: How the affiliate joined (e.g., `invitation`, `network`) * **`total_visits`**: Number of referral link visits * **`total_sales`**: Number of completed sales from referrals * **`total_earnings`**: Total commission earned with formatted amount * **`account`**: Nested affiliate account details including user information * **`account.user`**: The affiliate's user profile (name, email, etc.) * **`account.pseudo`**: Display name used by the affiliate * **`account.country`**: The affiliate's country with code and name * **`suspended_at`**: Timestamp when suspended (null if active) * **`suspended_reason`**: Reason for suspension (null if active) ## Affiliate Statuses | Status | Description | | ----------- | -------------------------------------------- | | `active` | Affiliate is active and can earn commissions | | `suspended` | Affiliate account has been suspended | ## Getting an Affiliate Retrieve a specific affiliate using their unique referral code: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/affiliates/CREATOR123" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/affiliates/CREATOR123', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { data } = await response.json(); ``` This is useful for: * Validating an affiliate code before applying commissions * Displaying affiliate information on your website * Building affiliate dashboards ## Sending Affiliate Invitations Invite potential affiliates to join your programme by sending invitation emails: ```bash cURL theme={null} curl -X POST "https://api.chariow.com/v1/affiliates/invitations" \ -H "Authorization: Bearer sk_live_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "emails": ["john@example.com", "jane@example.com", "partner@business.com"] }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/affiliates/invitations', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ emails: ['john@example.com', 'jane@example.com', 'partner@business.com'] }) }); const { data } = await response.json(); ``` ### Batch Invitations You can send up to 25 invitations in a single request. The API will: * Create and send invitations for valid new emails * Skip emails that are already registered affiliates * Skip emails that have pending invitations ### Response ```json theme={null} { "message": "2 invitations sent successfully", "data": [ { "id": "affinv_abc123xyz", "email": "john@example.com", "status": "pending", "expires_at": "2026-02-10T10:30:00+00:00", "invited_by": { "id": "tm_abc123", "name": "Store Owner" }, "accepted_at": null, "created_at": "2026-01-11T10:30:00+00:00" }, { "id": "affinv_def456uvw", "email": "jane@example.com", "status": "pending", "expires_at": "2026-02-10T10:30:00+00:00", "invited_by": { "id": "tm_abc123", "name": "Store Owner" }, "accepted_at": null, "created_at": "2026-01-11T10:30:00+00:00" } ], "errors": [] } ``` ## Invitation Object An invitation contains information about its status, who sent it, and expiration: ```json theme={null} { "id": "affinv_abc123xyz", "email": "john@example.com", "status": "pending", "expires_at": "2026-02-10T10:30:00+00:00", "invited_by": { "id": "tm_abc123", "name": "Store Owner" }, "accepted_at": null, "created_at": "2026-01-11T10:30:00+00:00" } ``` ### Invitation Statuses | Status | Description | | ----------- | ----------------------------------------- | | `pending` | Invitation sent but not yet accepted | | `accepted` | Invitation has been accepted | | `expired` | Invitation has passed its expiration date | | `cancelled` | Invitation has been cancelled | ## Webhook Events When an affiliate joins your store (accepts an invitation), a webhook event is triggered via [Pulses](/en/guides/pulses). ### affiliate.joined Triggered when a new affiliate joins your store. ```json theme={null} { "event": "affiliate.joined", "affiliate": { "id": "saff_xyz789abc", "account": { "id": "aff_abc123def", "code": "CREATOR123", "pseudo": "creative_studio", "email": "creator@example.com", "first_name": "John", "last_name": "Doe", "name": "John Doe", "country": { "code": "US", "name": "United States" }, "phone": { "country_code": "US", "formatted": "+1 234 567 890" } }, "source": "invitation", "status": "active", "joined_at": "2025-01-15T10:40:00+00:00" }, "store": { "id": "str_xyz789", "name": "My Digital Store", "url": "https://mystore.mychariow.com" } } ``` Configure Pulses in your Chariow dashboard (**Automation** > **Pulses**) to receive these events. ## Implementation Example Here's a complete example of managing affiliates in your application: ```javascript theme={null} class AffiliateManager { constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = 'https://api.chariow.com/v1'; } async getAffiliate(code) { const response = await fetch( `${this.baseUrl}/affiliates/${code}`, { headers: { 'Authorization': `Bearer ${this.apiKey}` }} ); if (!response.ok) { throw new Error('Affiliate not found'); } const { data } = await response.json(); return data; } async sendInvitations(emails) { const response = await fetch( `${this.baseUrl}/affiliates/invitations`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ emails }) } ); if (!response.ok) { const error = await response.json(); throw new Error(error.message); } return response.json(); } async validateAffiliateCode(code) { try { const affiliate = await this.getAffiliate(code); return { valid: affiliate.status === 'active', affiliate }; } catch (error) { return { valid: false, error: error.message }; } } } // Usage const manager = new AffiliateManager('sk_live_your_api_key'); // Validate an affiliate code at checkout const { valid, affiliate } = await manager.validateAffiliateCode('CREATOR123'); if (valid) { console.log(`Affiliate ${affiliate.account.user.name} is active`); console.log(`Total earnings: ${affiliate.total_earnings.formatted}`); } // Send invitations to potential affiliates const result = await manager.sendInvitations([ 'partner1@example.com', 'partner2@example.com' ]); console.log(`${result.data.length} invitations sent`); ``` ## API Endpoints Summary | Endpoint | Method | Description | | -------------------------------- | ------ | ----------------------------- | | `/v1/affiliates/{affiliateCode}` | GET | Get affiliate details by code | | `/v1/affiliates/invitations` | POST | Send affiliate invitations | ## Best Practices ### Affiliate Code Validation * Always validate affiliate codes before applying commissions * Check the affiliate status is `active` * Cache affiliate data to reduce API calls ### Invitation Management * Use batch invitations for efficiency (up to 25 emails) * Handle skipped emails gracefully * Implement retry logic for failed invitations ### Webhook Integration * Set up webhooks to receive `affiliate.joined` events * Use webhooks to trigger onboarding workflows * Store affiliate data when they join for faster lookups ### Commission Tracking * Track affiliate referrals accurately * Provide affiliates with real-time statistics * Implement proper attribution windows ## Related Resources Set up webhooks for affiliate events View the Get Affiliate API reference View the Send Invitations API reference Track affiliate sales # Best Practices Source: https://chariow.dev/en/guides/best-practices Production-ready guidelines for integrating with the Chariow Checkout API Follow these best practices to build robust, secure, and reliable integrations with the Chariow API, especially when handling checkout flows. ## Checkout API Best Practices ### Always Handle All Response States The checkout API returns different states that require different handling. Never assume a checkout will always require payment. ```javascript theme={null} async function handleCheckout(checkoutData) { const response = await fetch('https://api.chariow.com/v1/checkout', { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(checkoutData) }); const result = await response.json(); switch (result.data.step) { case 'awaiting_payment': // Redirect to payment - most common case return { action: 'redirect', url: result.data.payment.checkout_url }; case 'completed': // Free product - sale completed immediately return { action: 'success', saleId: result.data.purchase.id }; case 'already_purchased': // Customer already owns this product return { action: 'already_owned', message: result.data.message }; default: // Handle unexpected states gracefully console.error('Unexpected checkout state:', result.data.step); return { action: 'error', message: 'Unexpected response from payment system' }; } } ``` ### Validate Data Before Submitting Reduce failed checkouts by validating customer data on your end before calling the API. ```javascript theme={null} function validateCheckoutData(data) { const errors = []; // Email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!data.email || !emailRegex.test(data.email)) { errors.push('Valid email is required'); } // Name validation if (!data.first_name || data.first_name.length > 50) { errors.push('First name is required (max 50 characters)'); } if (!data.last_name || data.last_name.length > 50) { errors.push('Last name is required (max 50 characters)'); } // Phone validation - numeric only const phoneNumber = data.phone?.number?.replace(/\D/g, ''); if (!phoneNumber || phoneNumber.length < 6) { errors.push('Valid phone number is required'); } // Country code validation const validCountryCodes = ['US', 'CA', 'GB', 'FR', 'DE', 'ES', 'IT', /* ... */]; if (!data.phone?.country_code || !validCountryCodes.includes(data.phone.country_code)) { errors.push('Valid country code is required'); } return { valid: errors.length === 0, errors }; } ``` ### Store Sale IDs Immediately Always persist the sale ID as soon as you receive it. This is critical for customer support, refunds, and order tracking. ```javascript theme={null} async function processCheckout(checkoutData, orderId) { const result = await initiateCheckout(checkoutData); if (result.data.purchase?.id) { // Store immediately - before any redirect await database.orders.update(orderId, { chariow_sale_id: result.data.purchase.id, checkout_initiated_at: new Date() }); } return result; } ``` ### Use Pulses for Reliable Sale Confirmation Never rely solely on redirect URLs to confirm purchases. Payment pages can be closed, redirects can fail, and customers may not return to your site. The `redirect_url` is for user experience only. Always use [Pulses](/en/guides/pulses) (webhooks) for reliable sale confirmation. ```javascript theme={null} // Set up a Pulse endpoint for sale confirmation. // express.raw is required: the signature covers the raw body bytes. app.post('/webhooks/chariow', express.raw({ type: 'application/json' }), async (req, res) => { if (!verifySignature(req.body, req.header('x-chariow-signature'), SECRET)) { return res.status(401).send('Invalid signature'); } const payload = JSON.parse(req.body.toString('utf8')); switch (payload.event) { case 'successful.sale': await fulfillOrder(payload.sale.id); await sendConfirmationEmail(payload.customer.email); break; case 'abandoned.sale': await scheduleRecoveryEmail(payload.sale.id); break; } res.status(200).send('OK'); } ); ``` Deduplicate on the `x-pulse-delivery-id` header and see [Pulse Security](/en/guides/pulse-security) for the full signing contract. *** ## Security Best Practices ### Never Expose API Keys in Client Code API keys should only be used server-side. Never include them in JavaScript bundles, mobile apps, or any client-facing code. ```javascript theme={null} // Server-side (Node.js) const API_KEY = process.env.CHARIOW_API_KEY; app.post('/api/checkout', async (req, res) => { const response = await fetch('https://api.chariow.com/v1/checkout', { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(req.body) }); res.json(await response.json()); }); ``` ```javascript theme={null} // Client-side - NEVER DO THIS const API_KEY = 'sk_live_abc123'; // EXPOSED! fetch('https://api.chariow.com/v1/checkout', { headers: { 'Authorization': `Bearer ${API_KEY}` } }); ``` ### Validate Webhook Signatures Always verify that incoming webhooks are genuinely from Chariow by checking the signature. The signature is an HMAC-SHA256 of the **raw body bytes**, prefixed with `sha256=`. ```javascript theme={null} const crypto = require('crypto'); function verifySignature(rawBody, receivedSignature, secret) { // rawBody must be the untouched Buffer, never a re-serialised object: // JSON.stringify does not reproduce the escaped forward slashes Chariow sends. const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); const a = Buffer.from(receivedSignature ?? ''); const b = Buffer.from(expected); // timingSafeEqual throws on buffers of different lengths. return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` Each Pulse has its own signing secret, prefixed `whsec_`, available from the Pulse's Overview tab in the dashboard. It is not your API key. See [Pulse Security](/en/guides/pulse-security). ### Use Environment Variables Store all sensitive configuration in environment variables, never in code. ```bash theme={null} # .env file (never commit this) CHARIOW_API_KEY=sk_live_your_api_key CHARIOW_WEBHOOK_SECRET=whsec_your_webhook_secret ``` ```javascript theme={null} // Load from environment const config = { apiKey: process.env.CHARIOW_API_KEY, webhookSecret: process.env.CHARIOW_WEBHOOK_SECRET }; ``` *** ## Error Handling Best Practices ### Implement Comprehensive Error Handling Handle all possible error scenarios gracefully to provide a good user experience. ```javascript theme={null} async function safeCheckout(checkoutData) { try { const response = await fetch('https://api.chariow.com/v1/checkout', { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(checkoutData) }); // Handle HTTP errors if (!response.ok) { const error = await response.json(); switch (response.status) { case 401: console.error('API key invalid'); return { error: 'Payment system configuration error' }; case 404: return { error: 'Product not found or unavailable' }; case 422: // Validation errors - return field-specific messages return { error: 'Validation failed', fields: error.errors }; case 429: return { error: 'Too many requests. Please try again shortly.' }; default: return { error: 'Payment system unavailable. Please try again.' }; } } return await response.json(); } catch (networkError) { // Handle network failures console.error('Network error:', networkError); return { error: 'Unable to connect to payment system. Check your connection.' }; } } ``` ### Display User-Friendly Error Messages Map API errors to helpful messages that guide users to fix issues. ```javascript theme={null} const errorMessages = { 'email': 'Please enter a valid email address', 'phone.number': 'Please enter a valid phone number (digits only)', 'phone.country_code': 'Please select your country', 'address': 'Please enter your shipping address', 'city': 'Please enter your city', 'state': 'Please enter your state or region', 'country': 'Please select your country', 'zip': 'Please enter your postal code', 'discount_code': 'This discount code is invalid or has expired' }; function getFieldError(fieldName, apiErrors) { if (apiErrors[fieldName]) { return errorMessages[fieldName] || apiErrors[fieldName][0]; } return null; } ``` ### Log Errors for Debugging Maintain detailed logs for troubleshooting while keeping sensitive data secure. ```javascript theme={null} function logCheckoutError(error, checkoutData) { // Remove sensitive data before logging const safeData = { product_id: checkoutData.product_id, email: maskEmail(checkoutData.email), timestamp: new Date().toISOString(), error: error.message || error }; console.error('Checkout failed:', JSON.stringify(safeData)); // Send to monitoring service monitoring.captureError(error, { context: safeData }); } function maskEmail(email) { const [local, domain] = email.split('@'); return `${local.slice(0, 2)}***@${domain}`; } ``` *** ## Performance Best Practices ### Implement Request Timeouts Don't let API calls hang indefinitely. Set reasonable timeouts. ```javascript theme={null} async function checkoutWithTimeout(checkoutData, timeoutMs = 30000) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch('https://api.chariow.com/v1/checkout', { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(checkoutData), signal: controller.signal }); return await response.json(); } finally { clearTimeout(timeout); } } ``` ### Cache Product Data Reduce API calls by caching product information that doesn't change frequently. ```javascript theme={null} const productCache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes async function getProduct(productId) { const cached = productCache.get(productId); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data; } const response = await fetch(`https://api.chariow.com/v1/products/${productId}`, { headers: { 'Authorization': `Bearer ${API_KEY}` } }); const data = await response.json(); productCache.set(productId, { data: data.data, timestamp: Date.now() }); return data.data; } ``` ### Respect Rate Limits Handle rate limiting gracefully with exponential backoff. ```javascript theme={null} async function fetchWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options); if (response.status === 429) { const retryAfter = response.headers.get('Retry-After') || Math.pow(2, attempt); console.log(`Rate limited. Retrying after ${retryAfter}s`); await sleep(retryAfter * 1000); continue; } return response; } throw new Error('Max retries exceeded'); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } ``` *** ## Testing Best Practices ### Test All Checkout Scenarios Create comprehensive tests for every possible checkout outcome. ```javascript theme={null} describe('Checkout Integration', () => { test('handles paid product checkout', async () => { const result = await handleCheckout({ product_id: 'prd_paid_product', email: 'test@example.com', // ... }); expect(result.action).toBe('redirect'); expect(result.url).toContain('payment.chariow.com'); }); test('handles free product checkout', async () => { const result = await handleCheckout({ product_id: 'prd_free_product', email: 'test@example.com', // ... }); expect(result.action).toBe('success'); expect(result.saleId).toBeDefined(); }); test('handles already purchased product', async () => { const result = await handleCheckout({ product_id: 'prd_already_owned', email: 'existing@example.com', // ... }); expect(result.action).toBe('already_owned'); }); test('handles invalid product gracefully', async () => { const result = await handleCheckout({ product_id: 'prd_nonexistent', email: 'test@example.com', // ... }); expect(result.error).toBeDefined(); }); test('handles shipping required products', async () => { const result = await handleCheckout({ product_id: 'prd_physical', email: 'test@example.com', address: '123 Main St', city: 'New York', state: 'NY', country: 'US', zip: '10001', // ... }); expect(result.action).toBe('redirect'); }); }); ``` ### Use Staging Environment for Integration Tests Always test against a staging environment before deploying to production. ```javascript theme={null} const config = { development: { apiUrl: 'https://api.staging.chariow.com/v1', apiKey: process.env.CHARIOW_STAGING_KEY }, production: { apiUrl: 'https://api.chariow.com/v1', apiKey: process.env.CHARIOW_LIVE_KEY } }; const env = process.env.NODE_ENV || 'development'; const apiConfig = config[env]; ``` *** ## Checklist for Production Before going live, ensure you've completed this checklist: * [ ] API keys stored in environment variables * [ ] API calls made server-side only * [ ] Webhook signatures verified * [ ] HTTPS used for all endpoints * [ ] Sensitive data not logged * [ ] All HTTP status codes handled * [ ] Network errors caught * [ ] User-friendly error messages displayed * [ ] Errors logged for debugging * [ ] Rate limiting handled gracefully * [ ] All checkout states handled (payment, completed, already\_purchased) * [ ] Sale IDs stored immediately * [ ] Pulses configured for sale confirmation * [ ] Customer data validated before submission * [ ] Shipping fields included when required * [ ] Unit tests for checkout logic * [ ] Integration tests with staging API * [ ] All product types tested * [ ] Error scenarios tested * [ ] Load testing completed *** ## Next Steps Explore real-world integration examples Learn the complete checkout flow Set up webhooks for notifications Explore the complete API # Checkout Source: https://chariow.dev/en/guides/checkout Learn how to initiate and manage checkout sessions via the Chariow API The checkout API allows you to programmatically create purchase sessions for your customers. This is useful for custom storefronts, integrations, or automated sales flows. **Unsupported product types** β€” The following cannot be used to initiate a checkout through the API: * **Service** products * **Coaching** products * Products with **pay-what-you-want** pricing For these, redirect customers to your [Chariow storefront](https://chariow.com) or use the **Snap Widget** embed on your website. All sales initiated via the checkout API will have their **Channel** set to **"API"** on your store dashboard. This helps you identify and track sales originating from your API integrations separately from other channels like your storefront or Snap Widget. ## Repeat Purchases The ability to purchase a product multiple times depends on the product type: | Product Type | Repeat Purchase | Behaviour | | ---------------- | --------------- | --------------------------------------------------------------------------------------------------------- | | **License** | Always allowed | Customers can purchase license products multiple times. Each purchase generates a new unique license key. | | **Downloadable** | Blocked | Returns `already_purchased` if customer has an active access grant. | | **Course** | Blocked | Returns `already_purchased` if customer has an active access grant. | | **Bundle** | Blocked | Returns `already_purchased` if customer has an active access grant. | For blocked product types, if a customer's access has been **revoked** (e.g., after a refund), they will be able to purchase the product again. The system checks for **active** access grants only. ## Checkout Flow Overview The Chariow checkout API handles the complete purchase flow from initiation to completion: The product must be **published** before initiating a checkout. Unpublished products will return a 404 error. Call the `/checkout` endpoint with product ID and customer details Check the `step` field in the response: * **payment**: Redirect customer to `checkout_url` for payment * **completed**: Sale completed immediately (free products) * **already\_purchased**: Customer already owns this product Customer completes payment on the secure Chariow payment page Get notified of sale status changes via webhooks (recommended) Customer receives automatic access to files, licenses, courses, etc. ## Product Types Supported The checkout API supports the following Chariow product types: * **Downloadable Products**: Digital files (PDFs, software, media) * **Courses**: Educational content with lessons and chapters * **Licenses**: Software license keys with activation management * **Bundles**: Collections of multiple products **Service** and **Coaching** product types are not supported via the Public API. For these products, redirect customers to your Chariow store or use the Snap Widget. Products using pay-what-you-want pricing are also not supported. ## Initiating a Checkout Create a new checkout session: ```bash cURL theme={null} curl -X POST "https://api.chariow.com/v1/checkout" \ -H "Authorization: Bearer sk_live_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "product_id": "prd_abc123", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "discount_code": "SAVE20" }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: 'prd_abc123', email: 'customer@example.com', first_name: 'John', last_name: 'Doe', phone: { number: '1234567890', country_code: 'US' }, discount_code: 'SAVE20' }) }); const data = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( 'https://api.chariow.com/v1/checkout', headers={ 'Authorization': 'Bearer sk_live_your_api_key', 'Content-Type': 'application/json' }, json={ 'product_id': 'prd_abc123', 'email': 'customer@example.com', 'first_name': 'John', 'last_name': 'Doe', 'phone': { 'number': '1234567890', 'country_code': 'US' }, 'discount_code': 'SAVE20' } ) ``` ### Request Parameters | Parameter | Type | Required | Description | | -------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------ | | `product_id` | string | Yes | Product public ID or slug (e.g., `prd_abc123xyz` or `premium-course`) | | `email` | string | Yes | Customer email address (max 255 characters) | | `first_name` | string | Yes | Customer first name (max 50 characters) | | `last_name` | string | Yes | Customer last name (max 50 characters) | | `phone.number` | string | Yes | Phone number (numeric only) | | `phone.country_code` | string | Yes | ISO country code (e.g., "US", "FR", "GB") | | `discount_code` | string | No | Discount code to apply (max 100 characters) | | `campaign_id` | string | No | Campaign public ID or tracking code | | `custom_fields` | object | No | Custom field values (key-value pairs) | | `payment_currency` | string | No | Currency code (ISO 4217, e.g., "USD", "EUR") | | `redirect_url` | string | No | Custom redirect URL after payment completion (max 2048 characters) | | `custom_metadata` | object | No | Custom key-value metadata to store with the sale (max 10 keys, 255 chars per value). Included in Pulse webhook payloads. | | `customer_ip` | string | No | The buyer's IP address, IPv4 or IPv6 (e.g., "203.0.113.42"). See [Buyer IP address](#buyer-ip-address). | ### Shipping Address Fields When the product has "Require shipping address" enabled, you must include shipping address fields in your checkout request: | Parameter | Type | Required | Description | | --------- | ------ | ----------- | --------------------------------------------------- | | `address` | string | Conditional | Street address for shipping (max 255 characters) | | `city` | string | Conditional | City for shipping (max 100 characters) | | `state` | string | Conditional | State or region for shipping (max 100 characters) | | `country` | string | Conditional | Country code (ISO 3166-1 alpha-2, e.g., "US", "FR") | | `zip` | string | Conditional | Postal/ZIP code for shipping (max 20 characters) | These fields are **required** only when the product has shipping enabled. If shipping is not required, these fields are ignored. #### Example with Shipping Address ```json theme={null} { "product_id": "prd_abc123xyz", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "address": "123 Main Street", "city": "New York", "state": "NY", "country": "US", "zip": "10001" } ``` ## Checkout Response States The checkout response includes a `step` field indicating the current state: ### Awaiting Payment For paid products, you'll receive a payment URL: ```json theme={null} { "data": { "step": "payment", "message": null, "purchase": { "id": "sal_xyz789", "status": "payment", "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" } }, "payment": { "checkout_url": "https://payment.chariow.com/checkout?token=abc123", "transaction_id": "txn_def456" } } } ``` Redirect the customer to `checkout_url` to complete their payment. ### Completed (Free Products) For free products, the sale completes immediately: ```json theme={null} { "data": { "step": "completed", "message": null, "purchase": { "id": "sal_xyz789", "status": "completed" }, "payment": { "checkout_url": null, "transaction_id": null } } } ``` ### Already Purchased If the customer already owns the product: ```json theme={null} { "data": { "step": "already_purchased", "message": "You have already purchased this product", "purchase": null, "payment": null } } ``` ## Custom Redirect URLs You can specify a custom redirect URL to send customers to your own thank-you page after payment completion: ```bash theme={null} curl -X POST "https://api.chariow.com/v1/checkout" \ -H "Authorization: Bearer sk_live_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "product_id": "prd_abc123xyz", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "redirect_url": "https://yoursite.com/thank-you?sale={sale_id}" }' ``` The redirect URL must be a valid active URL (max 2048 characters). When not provided, customers will be redirected to the default Chariow post-purchase page. ## Multi-Currency Support Specify the payment currency to charge customers in a different currency from your store's default: ```json theme={null} { "product_id": "prd_abc123xyz", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "payment_currency": "EUR" } ``` The response will include exchange rate information when currency conversion is applied. ## Applying Discount Codes Pass a discount code to apply savings: ```json theme={null} { "product_id": "prd_abc123xyz", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "discount_code": "SAVE20" } ``` The response will show the discounted amount: ```json theme={null} { "data": { "purchase": { "original_amount": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "discount_amount": { "value": 19.80, "formatted": "$19.80", "short": "20", "currency": "USD" }, "discount": { "id": "dis_xyz789", "code": "SAVE20", "type": "percentage", "value": 20 } } } } ``` ## Custom Fields If your product has custom fields configured, you can collect and validate them during checkout: ```json theme={null} { "product_id": "prd_abc123xyz", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "custom_fields": { "company_name": "Acme Corp", "job_title": "Developer", "team_size": "10-50" } } ``` Custom fields must match the product's configured custom field definitions. Invalid or missing required custom fields will result in validation errors. ## Campaign Tracking Track the source of sales by including a campaign ID: ```json theme={null} { "product_id": "prd_abc123xyz", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "campaign_id": "camp_summer2024" } ``` This helps you: * Track which marketing campaigns drive the most sales * Attribute revenue to specific channels * Analyse campaign performance in your Chariow dashboard ## Custom Metadata Store custom data with the sale for your own tracking and integration purposes: ```json theme={null} { "product_id": "prd_abc123xyz", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "custom_metadata": { "order_ref": "ORD-123", "source": "landing_page", "utm_campaign": "summer_sale" } } ``` ### Important Notes * Maximum 10 keys allowed per sale * Each value is limited to 255 characters * Keys should be strings with alphanumeric characters and underscores * Custom metadata is included in Pulse webhook payloads Use custom metadata to link Chariow sales with your CRM, analytics platform, or internal systems. The metadata is returned in all sale-related webhooks. ## Buyer IP Address The checkout endpoint is called from your server, so the IP address we observe is your own infrastructure, not the buyer's. Pass the buyer's IP in `customer_ip` to correct that: ```json theme={null} { "product_id": "prd_abc123xyz", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "customer_ip": "203.0.113.42" } ``` Read the buyer's IP from the request that reaches your own server, typically the leftmost entry of the `X-Forwarded-For` header, or `CF-Connecting-IP` if you sit behind Cloudflare. ### What it improves * **Payment methods** β€” the buyer's country is resolved from this IP, and the country determines which payment methods appear on the checkout page * **Analytics** β€” sales are attributed to the buyer's country rather than to your server's hosting region * **Fraud review** β€” the sale record carries the buyer's real IP ### Important notes * The field is optional. Omit it and we fall back to the calling IP, exactly as before * Both IPv4 and IPv6 are accepted; a malformed value returns a `422` * The field is only honoured on API checkouts, so it cannot be spoofed from a browser ## Error Handling Common checkout errors and how to handle them: | HTTP Status | Error | Cause | Solution | | ----------- | ------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------ | | 401 | Unauthorised | Invalid or missing API key | Check your API key is correct and included in the `Authorization` header | | 404 | Product not found | Invalid product ID or unpublished product | Verify the product ID/slug exists and is published | | 422 | Validation failed | Missing or invalid required fields | Check all required fields are provided with correct formats | | 422 | Pay-what-you-want not supported | Product uses pay-what-you-want pricing | Redirect customers to your Chariow store or use the Snap Widget | | 422 | Product type not supported | Product is a Service or Coaching type | Redirect customers to your Chariow store or use the Snap Widget | | 422 | Invalid discount code | Discount code expired, invalid, or already used | Verify the discount code is active and applicable | | 422 | Missing shipping address | Product requires shipping but address fields are missing | Include `address`, `city`, `state`, `country`, and `zip` fields | ### Example Error Response ```json theme={null} { "message": "The email field must be a valid email address.", "data": [], "errors": { "email": [ "The email field must be a valid email address." ], "phone.number": [ "The phone.number field is required." ] } } ``` Always check the `errors` object for field-specific validation messages to help users correct their input. ## Best Practices ### Handle All Response Steps Always check the `step` field and handle all possible states: ```javascript theme={null} const response = await fetch('https://api.chariow.com/v1/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify(checkoutData) }); const result = await response.json(); switch (result.data.step) { case 'payment': // Redirect to payment URL window.location.href = result.data.payment.checkout_url; break; case 'completed': // Show success message for free products showSuccessMessage(result.data.purchase); break; case 'already_purchased': // Inform customer they already own this product showAlreadyPurchasedMessage(result.data.message); break; } ``` ### Use Pulses for Sale Updates Don't rely solely on redirect URLs to track sale completion. Set up Pulses (webhooks) to receive reliable notifications: * Sale completed * Payment received * Refund processed See the [Pulses Guide](/en/guides/pulses) for setup instructions. ### Validate Before Checkout Reduce failed checkouts by validating data before calling the API: * Email format validation * Phone number format validation * Required field checks * Custom field validation ### Handle Network Errors Implement proper error handling for network issues: ```javascript theme={null} try { const response = await fetch('https://api.chariow.com/v1/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify(checkoutData) }); if (!response.ok) { const error = await response.json(); handleCheckoutError(error); return; } const data = await response.json(); handleCheckoutSuccess(data); } catch (error) { // Handle network errors console.error('Checkout failed:', error); showErrorMessage('Unable to process checkout. Please try again.'); } ``` ### Store Sale IDs Always store the returned sale ID (`purchase.id`) for: * Customer service inquiries * Refund processing * Access management * Analytics tracking ### Test with Different Scenarios Test your integration with: * Free products (immediate completion) * Paid products (payment flow) * Products with discount codes * Products with custom fields * Invalid product IDs * Invalid discount codes ## Next Steps Learn how to retrieve and manage sales Set up notifications for completed sales View the complete Checkout API reference # Customers Source: https://chariow.dev/en/guides/customers Learn how to manage and retrieve customer data via the Chariow API Customers are created automatically when they make a purchase in your store. Each customer is specific to your store and has their own profile with contact information, purchase history, and product access. ## Customer Object A customer object contains the following information: ```json theme={null} { "id": "cus_abc123xyz", "name": "John Doe", "first_name": "John", "last_name": "Doe", "email": "john@example.com", "avatar_url": "https://cdn.chariow.com/avatars/abc123.jpg", "phone": { "number": "+1 234 567 890", "country": { "name": "United States", "code": "US", "alpha_3_code": "USA", "dial_code": "+1", "currency": "USD", "flag": "πŸ‡ΊπŸ‡Έ" } }, "store": { "id": "str_xyz789", "name": "My Digital Store", "logo_url": "https://cdn.chariow.com/stores/xyz789/logo.png", "url": "https://mystore.chariow.link" }, "created_at": "2025-01-15T10:30:00+00:00", "updated_at": "2025-01-20T14:45:00+00:00" } ``` ### Customer Properties | Property | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `id` | string | Unique customer identifier with `cus_` prefix | | `name` | string | Full name (combination of first and last name) | | `first_name` | string | Customer's first name | | `last_name` | string | Customer's last name | | `email` | string | Customer's email address | | `avatar_url` | string | URL to the customer's avatar image | | `phone` | object | Phone number with country information | | `store` | object | Store the customer belongs to | | `created_at` | string | ISO 8601 timestamp of when the customer was created | | `updated_at` | string | ISO 8601 timestamp of when the customer was last updated | ## Listing Customers Retrieve all customers for your store: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/customers" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/customers', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const result = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/customers', headers={'Authorization': 'Bearer sk_live_your_api_key'} ) result = response.json() customers = result['data'] ``` ### Query Parameters | Parameter | Type | Description | | ------------ | ------- | ---------------------------------------------------------------- | | `per_page` | integer | Number of customers per page (default: 15, max: 100) | | `cursor` | string | Cursor for pagination (use `next_cursor` from previous response) | | `search` | string | Search by name, email, or phone number | | `start_date` | string | Filter customers created from this date (Y-m-d format) | | `end_date` | string | Filter customers created until this date (Y-m-d format) | ### Searching Customers Search for customers by name, email, or phone number: ```bash theme={null} curl -X GET "https://api.chariow.com/v1/customers?search=john" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ### Filtering by Date Range Retrieve customers created within a specific date range: ```bash theme={null} curl -X GET "https://api.chariow.com/v1/customers?start_date=2025-01-01&end_date=2025-01-31" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ### Example Response ```json theme={null} { "data": [ { "id": "cus_abc123xyz", "name": "John Doe", "first_name": "John", "last_name": "Doe", "email": "john@example.com", "avatar_url": "https://cdn.chariow.com/avatars/abc123.jpg", "phone": { "number": "+1 234 567 890", "country": { "name": "United States", "code": "US", "alpha_3_code": "USA", "dial_code": "+1", "currency": "USD", "flag": "πŸ‡ΊπŸ‡Έ" } }, "store": { "id": "str_xyz789", "name": "My Digital Store", "logo_url": "https://cdn.chariow.com/stores/xyz789/logo.png", "url": "https://mystore.chariow.link" }, "created_at": "2025-01-15T10:30:00+00:00", "updated_at": "2025-01-20T14:45:00+00:00" } ], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true } } ``` ## Getting a Single Customer Retrieve a specific customer by their public ID: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/customers/cus_abc123" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/customers/cus_abc123', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const result = await response.json(); ``` ## Customer Lifecycle A new customer record is created when they make their first purchase. Each purchase is linked to the customer's profile. The customer receives access to their purchased products (downloads, courses, licenses). Customers can access the customer portal to view their purchases and downloads. ## Common Use Cases Sync customers to your CRM by fetching new customers using date filters: ```javascript theme={null} // Fetch customers created in the last 24 hours const today = new Date().toISOString().split('T')[0]; const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]; const response = await fetch( `https://api.chariow.com/v1/customers?start_date=${yesterday}&end_date=${today}`, { headers: { 'Authorization': 'Bearer sk_live_your_api_key' }} ); const result = await response.json(); // Sync result.data to your CRM ``` Add customers to your email lists based on their purchases: ```javascript theme={null} // Get customer details for email list const response = await fetch( 'https://api.chariow.com/v1/customers/cus_abc123xyz', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' }} ); const result = await response.json(); await addToMailingList({ email: result.data.email, firstName: result.data.first_name, lastName: result.data.last_name, tags: ['customer', 'purchased'] }); ``` Look up a customer by email when they contact support: ```bash theme={null} curl -X GET "https://api.chariow.com/v1/customers?search=customer@example.com" \ -H "Authorization: Bearer sk_live_your_api_key" ``` The search parameter matches against name, email, and phone number fields. Generate monthly customer acquisition reports using date filters: ```bash theme={null} curl -X GET "https://api.chariow.com/v1/customers?start_date=2025-01-01&end_date=2025-01-31&per_page=100" \ -H "Authorization: Bearer sk_live_your_api_key" ``` This retrieves all customers who made their first purchase in January 2025. ## Related Resources View customer purchase history Manage customer licenses View the complete Customers API reference # Discounts Source: https://chariow.dev/en/guides/discounts Learn how to manage discount codes via the Chariow API Discounts allow you to offer promotional pricing to your customers. Create percentage or fixed-amount discounts with optional usage limits, expiration dates, product restrictions, and customer-specific codes. ## Discount Object A discount contains the following information: ```json theme={null} { "id": "dis_abc123", "name": "Summer Sale", "code": "SUMMER20", "type": "percentage", "status": "active", "value_off": { "raw": 20, "formatted": "20%" }, "products": [ { "id": "prd_def456", "name": "Premium Course", "type": "course", "pictures": { "thumbnail": "https://cdn.chariow.com/products/thumb.jpg", "cover": "https://cdn.chariow.com/products/cover.jpg" }, "category": { "value": "education_and_learning", "label": "Education and Learning" }, "pricing": { "type": "one_time", "price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "effective": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" } }, "bundle": null, "metadata": null } ], "store": { "id": "str_xyz789", "name": "My Store", "logo_url": "https://cdn.chariow.com/stores/xyz789/logo.png", "url": "https://mystore.mychariow.com" }, "customer_email": null, "usage_limit": 100, "usage_count": 45, "start_date": "2025-06-01T00:00:00+00:00", "end_date": "2025-08-31T23:59:59+00:00", "is_auto_generated": false, "created_at": "2025-05-15T10:00:00+00:00", "updated_at": "2025-06-20T15:30:00+00:00" } ``` ## Discount Types | Type | Description | Example | | ------------ | ------------------------ | -------- | | `percentage` | Percentage off the price | 20% off | | `fixed` | Fixed amount off | \$10 off | ## Discount Statuses | Status | Description | | --------- | --------------------------------------------------------------------------- | | `active` | Discount is valid and can be used (within date range and under usage limit) | | `expired` | Discount has passed its end date or usage limit has been reached | ## Listing Discounts Retrieve all discounts for your store: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/discounts" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/discounts', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { data } = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/discounts', headers={'Authorization': 'Bearer sk_live_your_api_key'} ) discounts = response.json()['data'] ``` ### Query Parameters | Parameter | Type | Description | | ------------ | ------- | ------------------------------------------------------- | | `per_page` | integer | Number of discounts per page (max 100, default 15) | | `cursor` | string | Pagination cursor from previous response | | `status` | string | Filter by status (`active`, `expired`) | | `search` | string | Search by discount code, name, or public ID | | `start_date` | string | Filter discounts created from this date (Y-m-d format) | | `end_date` | string | Filter discounts created until this date (Y-m-d format) | ### Filtering Examples ```bash theme={null} # Get active discounts only curl -X GET "https://api.chariow.com/v1/discounts?status=active" \ -H "Authorization: Bearer sk_live_your_api_key" # Search for a specific code or name curl -X GET "https://api.chariow.com/v1/discounts?search=SUMMER" \ -H "Authorization: Bearer sk_live_your_api_key" # Filter by date range curl -X GET "https://api.chariow.com/v1/discounts?start_date=2025-01-01&end_date=2025-01-31" \ -H "Authorization: Bearer sk_live_your_api_key" # Combine multiple filters curl -X GET "https://api.chariow.com/v1/discounts?status=active&search=VIP&per_page=50" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ### Example Response ```json theme={null} { "message": "success", "data": { "data": [ { "id": "dis_abc123", "name": "Summer Sale", "code": "SUMMER20", "type": "percentage", "status": "active", "value_off": { "raw": 20, "formatted": "20%" }, "products": [ { "id": "prd_def456", "name": "Premium Course", "type": "course", "pictures": { "thumbnail": "https://cdn.chariow.com/products/thumb.jpg", "cover": "https://cdn.chariow.com/products/cover.jpg" }, "category": { "value": "education_and_learning", "label": "Education and Learning" }, "pricing": { "type": "one_time", "price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "effective": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" } }, "bundle": null, "metadata": null } ], "store": { "id": "str_xyz789", "name": "My Store", "logo_url": "https://cdn.chariow.com/stores/xyz789/logo.png", "url": "https://mystore.mychariow.com" }, "customer_email": null, "usage_count": 45, "usage_limit": 100, "start_date": "2025-06-01T00:00:00+00:00", "end_date": "2025-08-31T23:59:59+00:00", "is_auto_generated": false, "created_at": "2025-05-15T10:00:00+00:00", "updated_at": "2025-06-20T15:30:00+00:00" } ], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true } }, "errors": [] } ``` ## Getting a Single Discount Retrieve a specific discount by its public ID: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/discounts/dis_abc123" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/discounts/dis_abc123', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { data } = await response.json(); ``` ## Applying Discounts at Checkout Use a discount code when initiating checkout: ```bash theme={null} curl -X POST "https://api.chariow.com/v1/checkout" \ -H "Authorization: Bearer sk_live_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "product_id": "prd_abc123", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" }, "discount_code": "SUMMER20" }' ``` ## Discount Restrictions Discounts can have various restrictions: Limit how many times a discount can be used: ```json theme={null} { "usage_limit": 100, "usage_count": 45 } ``` When `usage_count` reaches `usage_limit`, the discount becomes invalid. Set start and end dates for the discount: ```json theme={null} { "start_date": "2025-06-01T00:00:00+00:00", "end_date": "2025-08-31T23:59:59+00:00" } ``` The discount is only valid between these dates. Limit the discount to specific products: ```json theme={null} { "products": [ { "id": "prd_abc123", "name": "Course A" }, { "id": "prd_def456", "name": "Course B" } ] } ``` If `products` is empty, the discount applies to all products. Limit the discount to a specific customer: ```json theme={null} { "customer_email": "vip@example.com" } ``` Only the specified customer can use this discount. ## Discount Validation When applying a discount at checkout, it's automatically validated against these criteria: 1. **Status** - Must be `active` 2. **Date range** - Current date must be within `start_date` and `end_date` (if specified) 3. **Usage limit** - `usage_count` must not have reached `usage_limit` (if specified) 4. **Product eligibility** - Product must be in the `products` array (if not empty, applies to all products) 5. **Customer eligibility** - Customer email must match `customer_email` (if specified) The discount status is automatically updated to `expired` when: * The `end_date` has passed * The current date is before `start_date` * The `usage_limit` has been reached If validation fails during checkout, the API will return an appropriate error message. ## Common Use Cases Track discount performance for marketing campaigns: ```javascript theme={null} const discounts = await getDiscounts({ status: 'active' }); const campaignStats = discounts.map(d => ({ code: d.code, uses: d.usage_count, remaining: d.usage_limit - d.usage_count, conversionRate: (d.usage_count / d.usage_limit * 100).toFixed(1) + '%' })); ``` Create unique discount codes for affiliates and track usage: ```javascript theme={null} // Fetch discount by public ID const response = await fetch('https://api.chariow.com/v1/discounts/dis_affiliate123', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { data: discount } = await response.json(); console.log(`Affiliate code ${discount.code} has been used ${discount.usage_count} times`); ``` Create single-use discounts for VIP customers: ```json theme={null} { "code": "VIP_CUSTOMER_ABC", "customer_email": "vip@example.com", "usage_limit": 1 } ``` ## Related Resources Apply discounts during checkout View the complete Discounts API reference # Licenses Source: https://chariow.dev/en/guides/licenses Learn how to manage software licenses via the Chariow API Chariow provides built-in license key management for software products. Licenses are automatically generated when customers purchase license-type products and can be activated, validated, and revoked through the API. ## License Object A license contains comprehensive information about its status, activation history, and validity: ```json theme={null} { "id": "lic_abc123", "status": "active", "customer": { "id": "cus_xyz789", "name": "John Doe", "email": "john@example.com" }, "product": { "id": "prd_abc456", "name": "Premium Software License", "slug": "premium-software-license" }, "license": { "key": "ABC-123-XYZ-789", "masked_key": "ABC-***-***-789" }, "is_active": true, "is_expired": false, "can_activate": true, "activations": { "count": 3, "max": 10, "remaining": 7 }, "certificate_url": "https://api.chariow.com/certificates/lic_abc123.pdf", "metadata": null, "activated_at": "2025-01-15T10:30:00.000000Z", "expires_at": "2026-01-15T10:30:00.000000Z", "expired_at": null, "revoked_at": null, "created_at": "2025-01-15T09:00:00.000000Z", "updated_at": "2025-01-15T10:30:00.000000Z" } ``` ### Key Fields * **`license.key`**: The unique license key string (e.g., `ABC-123-XYZ-789`) * **`license.masked_key`**: Masked version for display (e.g., `ABC-***-***-789`) * **`status`**: Current license status (see table below) * **`is_active`**: Boolean indicating if the license is currently active and usable * **`is_expired`**: Boolean indicating if the license has expired * **`can_activate`**: Boolean indicating if the license can be activated (has remaining slots) * **`activations.count`**: Current number of device activations * **`activations.max`**: Maximum allowed activations * **`activations.remaining`**: Number of activation slots remaining * **`certificate_url`**: URL to download the license certificate (if available) * **`activated_at`**: When the license was first activated (null if never activated) * **`expires_at`**: When the license will expire (null for lifetime licenses) * **`revoked_at`**: When the license was revoked (null if not revoked) ## License Statuses | Status | Description | | -------------------- | -------------------------------------- | | `pending_activation` | License created but not yet activated | | `active` | License is activated and valid for use | | `expired` | License has passed its expiration date | | `revoked` | License has been permanently revoked | ## Listing Licenses Retrieve all issued licenses for your store: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/licenses" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/licenses', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { data } = await response.json(); ``` ### Query Parameters | Parameter | Type | Description | | ------------- | ------- | ----------------------------------------------------------------------- | | `per_page` | integer | Number of licenses per page (default 50, max 100) | | `cursor` | string | Pagination cursor from previous response | | `status` | string | Filter by status (`pending_activation`, `active`, `expired`, `revoked`) | | `customer_id` | string | Filter by customer public ID (e.g., `cus_abc123`) | | `product_id` | string | Filter by product public ID (e.g., `prd_def456`) | ### Filtering Examples ```bash theme={null} # Get active licenses only curl -X GET "https://api.chariow.com/v1/licenses?status=active" \ -H "Authorization: Bearer sk_live_your_api_key" # Get licenses for a specific product curl -X GET "https://api.chariow.com/v1/licenses?product_id=prd_abc123" \ -H "Authorization: Bearer sk_live_your_api_key" # Get licenses for a specific customer curl -X GET "https://api.chariow.com/v1/licenses?customer_id=cus_xyz789" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ## Getting a License by Key Retrieve a specific license using its license key: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/licenses/ABC-123-XYZ-789" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/licenses/ABC-123-XYZ-789', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const { data } = await response.json(); ``` ## Validating a License To validate a license in your application, retrieve it and check the status: ```javascript theme={null} async function validateLicense(licenseKey) { const response = await fetch( `https://api.chariow.com/v1/licenses/${licenseKey}`, { headers: { 'Authorization': 'Bearer sk_live_your_api_key' }} ); if (!response.ok) { return { valid: false, reason: 'License not found' }; } const { data } = await response.json(); if (!data.is_active) { return { valid: false, reason: 'License is not active' }; } if (data.is_expired) { return { valid: false, reason: 'License has expired' }; } return { valid: true, license: data }; } ``` ## Activating a License Activate a license on a device. The system automatically captures IP address and user agent: ```bash cURL theme={null} curl -X POST "https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/activate" \ -H "Authorization: Bearer sk_live_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "device_identifier": "00:1B:44:11:3A:B7" }' ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/activate', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ device_identifier: '00:1B:44:11:3A:B7' // MAC address, UUID, etc. }) } ); ``` ### First Activation Behaviour On the first activation: * Status changes from `pending_activation` to `active` * `activated_at` timestamp is set * `expires_at` is calculated based on product validity period * `activation_count` increments to 1 ### Subsequent Activations On additional activations: * `activation_count` is incremented * A new activation record is created * License status remains `active` ### Licences That Need No Activation A product can be configured so that its licences never require an activation call. Set `requires_activation` to `false` in the product's licence settings from your Chariow dashboard. This is the mode for SaaS products and services that police their own devices and have no reason to call back into Chariow. From then on, every licence the product issues: * is created **already active** β€” the status is `active`, never `pending_activation`; * has `activated_at` stamped at issuance; * starts its validity period at issuance, so `expires_at` is computed from the purchase date rather than from a first activation; * emits both the `license.issued` and the `license.activated` Pulse events at once. Licences issued **before** the setting was turned off keep their current state. One that is still `pending_activation` can be activated as usual. Calling the activation endpoint on a licence that is already active under this mode returns `400`: ```json theme={null} { "message": "This license does not require activation and is already active.", "data": [], "errors": [] } ``` Validate such licences with `GET /v1/licenses/{licenseKey}` instead of activating them. ### Activation Limits Each license has a maximum number of activations (`max_activations`). When the limit is reached, further activations will fail: ```json theme={null} { "message": "Activation limit reached", "data": [], "errors": [] } ``` Check `can_activate` and `activations.remaining` before attempting activation. ## Revoking a License Revoke a license to prevent further use: ```bash cURL theme={null} curl -X POST "https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/revoke" \ -H "Authorization: Bearer sk_live_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "reason": "Customer requested refund" }' ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/revoke', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'Customer requested refund' }) } ); ``` Revoking a license is permanent. The license cannot be reactivated after revocation. ## Getting Activation History View all activations for a specific license with detailed device tracking: ```bash theme={null} curl -X GET "https://api.chariow.com/v1/licenses/ABC-123-XYZ-789/activations?per_page=20" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ### Response ```json theme={null} { "data": [ { "id": "act_abc123", "activated_by": { "ip": { "value": "203.0.113.45", "country": { "code": "US", "name": "United States" } }, "user_agent": { "browser": "Chrome", "platform": "Windows", "device": "desktop" }, "device": "00:1B:44:11:3A:B7" }, "metadata": null, "activated_at": "2025-01-18T15:42:00.000000Z", "created_at": "2025-01-18T15:42:00.000000Z" }, { "id": "act_def456", "activated_by": { "ip": { "value": "198.51.100.12", "country": { "code": "FR", "name": "France" } }, "user_agent": { "browser": "Safari", "platform": "macOS", "device": "desktop" }, "device": "A4:5E:60:D8:2F:11" }, "metadata": null, "activated_at": "2025-01-16T09:15:00.000000Z", "created_at": "2025-01-16T09:15:00.000000Z" } ], "pagination": { "count": 2, "per_page": 20, "next_cursor": null, "prev_cursor": null, "has_more_pages": false } } ``` ### What's Tracked Each activation record contains: * **IP Address**: Automatically captured from the activation request, with geolocation * **User Agent**: Parsed browser and platform information * **Device Identifier**: Optional identifier you provide (MAC address, UUID, etc.) * **Timestamp**: When the activation occurred * **Metadata**: Optional custom data This is useful for: * Auditing license usage * Detecting suspicious activity * Providing customers with device management * Debugging activation issues ## Implementation Example Here's a complete example of license validation in a desktop application: ```javascript theme={null} class LicenseManager { constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = 'https://api.chariow.com/v1'; } async validate(licenseKey) { const response = await fetch( `${this.baseUrl}/licenses/${licenseKey}`, { headers: { 'Authorization': `Bearer ${this.apiKey}` }} ); if (!response.ok) { throw new Error('Invalid license key'); } const { data } = await response.json(); if (!data.can_activate) { throw new Error('License cannot be activated'); } return data; } async activate(licenseKey, deviceId) { const response = await fetch( `${this.baseUrl}/licenses/${licenseKey}/activate`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ device_identifier: deviceId }) } ); if (!response.ok) { const error = await response.json(); throw new Error(error.message); } return response.json(); } } // Usage const manager = new LicenseManager('sk_live_your_api_key'); try { const license = await manager.validate('ABC-123-XYZ-789'); console.log('License valid:', license.is_active); await manager.activate('ABC-123-XYZ-789', getDeviceId()); console.log('License activated successfully'); } catch (error) { console.error('License error:', error.message); } ``` ## API Endpoints Summary | Endpoint | Method | Description | | --------------------------------------- | ------ | -------------------------------- | | `/v1/licenses` | GET | List all licenses with filtering | | `/v1/licenses/{licenseKey}` | GET | Get specific license details | | `/v1/licenses/{licenseKey}/activate` | POST | Activate license on a device | | `/v1/licenses/{licenseKey}/revoke` | POST | Permanently revoke a license | | `/v1/licenses/{licenseKey}/activations` | GET | Get activation history | ## Best Practices ### Security * Never expose your API key in client-side code * Use API keys server-side only * Validate licenses server-side before granting access * Store license keys securely on the customer's device ### Device Identification * Use unique, persistent device identifiers (MAC address, hardware UUID) * Don't use user-changeable identifiers (computer name, username) * Consider platform-specific identifiers (Windows: machine GUID, macOS: hardware UUID) ### Activation Management * Check `can_activate` before attempting activation * Display `activations_remaining` to users * Implement device management UI for customers * Handle activation errors gracefully ### Validation Frequency * Validate on application startup * Re-validate periodically (e.g., every 24 hours) * Cache validation results locally * Implement offline grace period ### Error Handling * Handle network errors gracefully * Provide clear error messages to users * Implement retry logic with exponential backoff * Log activation attempts for debugging ## Related Resources Create license-type products View the complete Licenses API reference Retrieve license details Activate on a device # Products Source: https://chariow.dev/en/guides/products Learn how to retrieve and work with products via the Chariow API Products are the core of your Chariow store. This guide covers how to retrieve and work with products through the Public API. ## Product Types Chariow supports several product types: | Type | Description | | -------------- | --------------------------------------------------------------------- | | `downloadable` | Digital files that customers can download after purchase | | `course` | Online courses with structured chapters and lessons | | `license` | Software licenses with activation and validation management | | `service` | Digital services, consultations, or custom work | | `bundle` | A collection of multiple products sold together at a discounted price | | `coaching` | Coaching or mentoring sessions | ## Product Categories Products are organised into the following categories: | Category | Value | | ------------------------- | --------------------------- | | Creative Arts | `creative_arts` | | Technology | `technology` | | Business and Finance | `business_and_finance` | | Personal Development | `personal_development` | | Education and Learning | `education_and_learning` | | Entertainment | `entertainment` | | Health and Wellness | `health_and_wellness` | | Literature and Publishing | `literature_and_publishing` | | Media and Communication | `media_and_communication` | | Miscellaneous | `miscellaneous` | ## Product States Products can be in different states: * **Draft** - Not visible to customers, still being edited * **Published** - Available for purchase on your store * **Archived** - No longer available but kept for records The Public API only returns **published** products. Draft and archived products are not accessible via the API. ## Listing Products Retrieve all published products for your store with optional filtering: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/products?per_page=20&type=course" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/products?per_page=20&type=course', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const result = await response.json(); console.log(result.data); // Array of products ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/products', params={'per_page': 20, 'type': 'course'}, headers={'Authorization': 'Bearer sk_live_your_api_key'} ) products = response.json()['data'] ``` ### Query Parameters | Parameter | Type | Description | | ---------- | ------- | ----------------------------------------------------------------- | | `per_page` | integer | Number of products per page (default: 10, max: 100) | | `cursor` | string | Pagination cursor from previous response | | `search` | string | Search by product name or slug | | `category` | string | Filter by category (e.g., `technology`, `education_and_learning`) | | `type` | string | Filter by type (e.g., `course`, `license`, `bundle`) | ### Pagination The API uses cursor-based pagination for efficient data retrieval: ```javascript theme={null} let allProducts = []; let cursor = null; do { const url = cursor ? `https://api.chariow.com/v1/products?cursor=${cursor}` : 'https://api.chariow.com/v1/products'; const response = await fetch(url, { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const result = await response.json(); allProducts.push(...result.data); cursor = result.pagination.next_cursor; } while (cursor); console.log(`Retrieved ${allProducts.length} products`); ``` ### Example Response ```json theme={null} { "data": [ { "id": "prd_abc123", "name": "Complete Web Development Course", "slug": "web-development-course", "description": "A comprehensive course covering modern web development...", "type": "course", "category": { "value": "education_and_learning", "label": "Education and Learning" }, "status": "published", "is_free": false, "pictures": { "thumbnail": "https://cdn.chariow.com/products/thumb.jpg", "cover": "https://cdn.chariow.com/products/cover.jpg" }, "pricing": { "type": "one_time", "price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "current_price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "effective": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "sale_price": null, "minimum_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "suggested_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "price_off": null }, "has_variant_pricing": false, "quantity": null, "settings": { "is_shipping_address_required": false }, "rating": { "average": 4.8, "count": 245 }, "on_sale_until": null, "sales_count": { "raw": 1250, "formatted": "1,250" }, "seo": null, "custom_cta_text": { "value": null, "label": null }, "fields": null, "store": null, "bundle": null } ], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true } } ``` ## Getting a Single Product Retrieve detailed information about a specific product by its public ID or slug: ```bash cURL (by ID) theme={null} curl -X GET "https://api.chariow.com/v1/products/prd_abc123" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```bash cURL (by slug) theme={null} curl -X GET "https://api.chariow.com/v1/products/web-development-course" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} // By public ID const response = await fetch('https://api.chariow.com/v1/products/prd_abc123', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const product = (await response.json()).data; console.log(product.name, product.pricing); ``` ```python Python theme={null} import requests # By public ID response = requests.get( 'https://api.chariow.com/v1/products/prd_abc123', headers={'Authorization': 'Bearer sk_live_your_api_key'} ) product = response.json()['data'] ``` ### Product Details The single product endpoint returns comprehensive information including: * **Pricing**: Current price, base price, effective price, sale price (if applicable), minimum price, suggested price, and discount percentage (`price_off`) * **Images**: Thumbnail and cover images via `pictures` * **Category**: Product category with value and label * **Variant Pricing**: Whether the product has variant pricing (`has_variant_pricing`) * **Ratings**: Average rating and number of reviews via `rating` * **Sales**: Sales count (if not hidden) via `sales_count` * **Quantity**: Stock information (if product has limited quantity) * **Settings**: Product settings such as `is_shipping_address_required` * **Sale Expiration**: `on_sale_until` datetime for temporary sale pricing * **Custom CTA Text**: Custom call-to-action text via `custom_cta_text` * **Bundle**: Savings information for bundle products * **Custom Fields**: Additional product fields via `fields` (when loaded) * **SEO**: SEO metadata via `seo` (when loaded) * **Store**: Store information (when loaded) ## Pricing Types Products can have different pricing models: A fixed price that customers pay once to access the product. This is the most common pricing type. ```json theme={null} { "pricing": { "type": "one_time", "price": { "value": 49, "formatted": "$49.00", "short": "49", "currency": "USD" }, "current_price": { "value": 49, "formatted": "$49.00", "short": "49", "currency": "USD" }, "effective": { "value": 49, "formatted": "$49.00", "short": "49", "currency": "USD" }, "sale_price": null, "minimum_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "suggested_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "price_off": null } } ``` Customers choose how much to pay, with an optional minimum and suggested price. Useful for donations, tip-ware, or customer-driven pricing. ```json theme={null} { "pricing": { "type": "what_you_want", "price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "current_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "effective": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "sale_price": null, "minimum_price": { "value": 5, "formatted": "$5.00", "short": "5", "currency": "USD" }, "suggested_price": { "value": 25, "formatted": "$25.00", "short": "25", "currency": "USD" }, "price_off": null } } ``` Products available at no cost, often used for lead generation, freebies, or sample content. ```json theme={null} { "pricing": { "type": "free", "price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "current_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "effective": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "sale_price": null, "minimum_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "suggested_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "price_off": null }, "is_free": true } ``` Products can have temporary sale prices with an expiration date. The `current_price` reflects the active price. ```json theme={null} { "pricing": { "type": "one_time", "price": { "value": 149, "formatted": "$149.00", "short": "149", "currency": "USD" }, "current_price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "effective": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "sale_price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "minimum_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "suggested_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "price_off": "34%" }, "on_sale_until": "2025-02-28T23:59:59Z" } ``` ## Product Bundles Bundles combine multiple products at a discounted price. When retrieving a bundle product, you'll receive information about the total bundle value and savings: ```json theme={null} { "id": "prd_bundle123", "name": "Complete Developer Bundle", "slug": "developer-bundle", "type": "bundle", "pricing": { "type": "one_time", "price": { "value": 199, "formatted": "$199.00", "short": "199", "currency": "USD" }, "current_price": { "value": 199, "formatted": "$199.00", "short": "199", "currency": "USD" }, "effective": { "value": 199, "formatted": "$199.00", "short": "199", "currency": "USD" }, "sale_price": null, "minimum_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "suggested_price": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "price_off": null }, "bundle": { "value": { "value": 297, "formatted": "$297.00", "short": "297", "currency": "USD" }, "savings": { "amount": { "value": 98, "formatted": "$98.00", "short": "98", "currency": "USD" }, "percentage": "33%" } } } ``` The `bundle.value` shows the total value if all products were purchased separately, whilst the `pricing.current_price` shows the discounted bundle price. The `bundle.savings` shows how much customers save by purchasing the bundle. ## Working with Product Data ### Filtering Products You can combine multiple filters to refine your product queries: ```javascript theme={null} // Get all free courses const response = await fetch( 'https://api.chariow.com/v1/products?type=course&category=education_and_learning', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } } ); // Search for products const searchResponse = await fetch( 'https://api.chariow.com/v1/products?search=web+development', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } } ); ``` ### Understanding Ratings Products include rating information with the average score and total count: ```json theme={null} { "rating": { "average": 4.8, "count": 245 } } ``` * **average**: Rating from 0 to 5 * **count**: Total number of ratings received ### Stock Quantity For products with limited stock, the `quantity` field provides detailed information: ```json theme={null} { "quantity": { "value": 100, "remaining": { "value": 35, "percent": "35%" }, "sold": { "value": 65, "percent": "65%" }, "total": 100 } } ``` Products without limited stock will have `quantity: null`. ### Price Formatting All price objects include four fields for flexible display: * **value**: Numeric amount (e.g., `99` for \$99.00) * **formatted**: Ready-to-display string (e.g., `$99.00`, `Β£99.00`, `€99.00`) * **short**: Abbreviated human-readable string using `forHumans` (e.g., `99`, `5K`, `1.25M`) * **currency**: ISO currency code (e.g., `USD`, `EUR`, `GBP`) ```javascript theme={null} // Using the formatted price const product = response.data; console.log(`Buy now for ${product.pricing.current_price.formatted}`); const price = product.pricing.current_price.value; if (price < 50) { console.log('Affordable option!'); } ``` ## Common Use Cases ### Building a Product Catalogue ```javascript theme={null} async function buildProductCatalogue(category) { const products = []; let cursor = null; do { const params = new URLSearchParams({ per_page: 50, category: category, ...(cursor && { cursor }) }); const response = await fetch( `https://api.chariow.com/v1/products?${params}`, { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } } ); const result = await response.json(); products.push(...result.data); cursor = result.pagination.next_cursor; } while (cursor); return products; } // Get all technology products const techProducts = await buildProductCatalogue('technology'); ``` ### Displaying Sale Products ```javascript theme={null} async function getSaleProducts() { const response = await fetch('https://api.chariow.com/v1/products?per_page=100', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const result = await response.json(); // Filter products currently on sale return result.data.filter(product => product.pricing.sale_price !== null && product.on_sale_until && new Date(product.on_sale_until) > new Date() ); } ``` ### Finding Popular Products ```javascript theme={null} async function getPopularProducts(minRating = 4.5, minReviews = 50) { const response = await fetch('https://api.chariow.com/v1/products?per_page=100', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const result = await response.json(); return result.data .filter(p => p.rating.average >= minRating && p.rating.count >= minReviews) .sort((a, b) => b.rating.average - a.rating.average); } ``` ## Best Practices Always use cursor-based pagination instead of fetching all products at once. This ensures efficient data retrieval and prevents timeouts. Product data doesn't change frequently. Consider caching products locally and refreshing periodically to reduce API calls. Not all products have thumbnail or cover images. Always check for `null` values before displaying images. Use the `formatted` field from price objects for display purposes. This ensures proper currency formatting and symbols. When displaying sale prices, verify that `on_sale_until` is in the future to avoid showing expired sales. ## Next Steps View the List Products API reference View the Get Product API reference Learn how to create checkout sessions Learn about API authentication # Pulse Security Source: https://chariow.dev/en/guides/pulse-security Verify that a Pulse genuinely came from Chariow, and deduplicate retries safely Every Pulse Chariow sends is signed with a secret that belongs to that Pulse alone. Verifying that signature is the only way to know a request on your endpoint genuinely originates from Chariow β€” your endpoint URL is public, so anyone who discovers it can post to it. This page is the complete signing contract. Implement it once and you can trust every payload you receive. ## Request headers Each delivery carries these headers: | Header | Contents | | --------------------- | -------------------------------------------------- | | `x-chariow-signature` | `sha256=` β€” the signature to verify | | `x-pulse-id` | The Pulse identifier, e.g. `pulse_abc123` | | `x-pulse-delivery-id` | The delivery identifier β€” **your idempotency key** | | `x-pulse-event` | The event name, e.g. `successful.sale` | | `Content-Type` | `application/json; charset=utf-8` | | `User-Agent` | `Pulse \| Chariow +https://chariow.com` | Test events sent from the dashboard carry `x-pulse-id` and `x-pulse-event` but **no** `x-pulse-delivery-id`, because no delivery record is created for them. Their payload also contains an extra `note` field. Real events always carry all four headers. ## The signing secret Each Pulse has its own signing secret, prefixed `whsec_`, generated by Chariow when the Pulse is created. The signing secret is **not** your API key, and it is not derived from your API key, your store identifier, or any other integration credential. It is an independent value. Using anything else to compute the HMAC will never produce a matching signature. To retrieve it: **Automations β†’ Pulses β†’ select your Pulse β†’ Overview tab β†’ Signing secret**. It is masked by default; use *Reveal*, *Copy* and *Rotate* on that block. The secret is encrypted at rest and never appears in API listings or request logs β€” those expose only a masked form such as `whsec_β€’β€’β€’β€’a1b2`. The plaintext value is returned only on an explicit reveal. ### Rotating the secret Rotation takes effect immediately and cannot be undone: the previous secret stops working the moment you rotate. Update your endpoint configuration in the same window. Deliveries dispatched before the rotation keep the signature computed with the previous secret, so adopt the new secret before replaying old deliveries. ## Signature scheme | Property | Value | | ----------- | -------------------------------------------- | | Algorithm | HMAC-SHA256 | | Header | `x-chariow-signature` | | Format | `sha256=<64 lowercase hex characters>` | | Signed data | The raw HTTP body bytes, exactly as received | | Key | The Pulse signing secret (`whsec_...`) | | Timestamp | None, by design | ``` signature = "sha256=" + hex( hmac_sha256( raw_request_body, pulse_secret ) ) ``` **Only the raw body is signed.** The HTTP method, the URL, the headers, `x-pulse-id`, `x-pulse-delivery-id` and any timestamp are all excluded from the computation. ### Body encoding The body is compact UTF-8 JSON: * no indentation whitespace and no newlines; * forward slashes are escaped β€” `https:\/\/example.com`; * non-ASCII characters are escaped as `\uXXXX`, so the transmitted body is ASCII in practice; * key order is the order in the transmitted body. Never re-serialise the parsed payload. `JSON.stringify(req.body)` or `json.dumps(payload)` will not reproduce these bytes β€” the escaped forward slashes alone are enough to break the digest. Capture the raw body **before** any JSON parsing. ### Comparing the signature Strip the `sha256=` prefix and compare against the bare hex digest, or rebuild `"sha256=" + digest` and compare the whole string. Either works as long as you are consistent. Always compare in constant time (`crypto.timingSafeEqual`, `hash_equals`, `hmac.compare_digest`) and check that both values have the same length first β€” `timingSafeEqual` throws on buffers of different sizes. The prefix identifies the algorithm so the scheme can evolve without breaking existing receivers. Treat any value that does not start with `sha256=` as a scheme you do not yet support. Reject any request without a valid `x-chariow-signature` header with a `401`. ## Verification examples ```javascript Node.js / Express theme={null} const crypto = require('crypto'); app.post('/webhooks/chariow', express.raw({ type: 'application/json' }), (req, res) => { const received = req.header('x-chariow-signature') ?? ''; const expected = 'sha256=' + crypto .createHmac('sha256', process.env.CHARIOW_PULSE_SECRET) .update(req.body) // raw Buffer, never a re-serialised object .digest('hex'); const a = Buffer.from(received); const b = Buffer.from(expected); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(401).send('Invalid signature'); } const deliveryId = req.header('x-pulse-delivery-id'); if (alreadyProcessed(deliveryId)) { return res.status(200).send('OK'); } res.status(200).send('OK'); enqueue(deliveryId, JSON.parse(req.body.toString('utf8'))); } ); ``` ```php PHP theme={null} ## Idempotency and replay protection The signature carries no timestamp, and that is deliberate. It is computed once when the delivery is dispatched and reused unchanged by every retry. A time window would wrongly reject a legitimately delayed retry β€” the last attempt of a delivery can land nearly three hours after the first. Replay protection is handled by `x-pulse-delivery-id` instead. That identifier is stable across every attempt of the same delivery, which makes it a ready-made idempotency key: 1. Read `x-pulse-delivery-id`. 2. If you have already processed it, return `200` and stop. 3. Otherwise persist it, return `200`, and process asynchronously. Deduplicate on `x-pulse-delivery-id`, not on the entity identifier inside the payload. A single sale can legitimately produce several deliveries β€” one per subscribed Pulse, plus any manual replay you trigger yourself. A manually replayed delivery is a new delivery with its own `x-pulse-delivery-id`, so it will pass your idempotency check and be processed again. That is the intended behaviour: replay exists to let you reprocess an event your endpoint missed. ## Troubleshooting a signature mismatch | Symptom | Likely cause | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | No representation of the digest ever matches | Wrong secret β€” an API key or a store identifier instead of the Pulse `whsec_...` secret | | Matches sometimes, fails on payloads with URLs or accented characters | The payload was re-serialised instead of hashed raw; escaped forward slashes or `\uXXXX` sequences were lost | | Every signature fails since a recent change | The secret was rotated; the endpoint is still using the old value | | Comparison throws instead of returning false | `timingSafeEqual` on buffers of different lengths β€” the `sha256=` prefix was stripped on one side only | | Signature verifies, but you process the same event twice | Missing deduplication on `x-pulse-delivery-id` | Use the **Deliveries** tab on the Pulse detail page to replay a real delivery while you debug. It shows the exact payload sent and the exact response your endpoint returned, which is far more useful than a test event for validating verification end to end. ## Related resources Events, payloads, delivery history and retries Security and reliability recommendations # Pulses Source: https://chariow.dev/en/guides/pulses Receive real-time notifications about events in your Chariow store Pulses are webhook notifications that allow you to receive real-time HTTP POST requests when specific events occur in your Chariow store. Instead of constantly polling the API, Pulses push event data to your configured endpoint URL as soon as something happens, enabling you to build responsive integrations and automations. ## How Pulses Work An event happens in your store (e.g., a sale is completed). Chariow sends a signed HTTP POST request to your configured endpoint. Your server checks the `x-chariow-signature` header before trusting the payload. Your server deduplicates on `x-pulse-delivery-id` and processes the payload. Your server returns a 2xx status code to confirm receipt. Your endpoint URL is public, so anyone who discovers it can post to it. Always verify the signature before acting on a payload β€” see [Pulse Security](/en/guides/pulse-security). ## Request Headers Every delivery carries these headers: | Header | Contents | | --------------------- | --------------------------------------------------- | | `x-chariow-signature` | `sha256=` β€” HMAC-SHA256 of the raw body | | `x-pulse-id` | The Pulse identifier, e.g. `pulse_abc123` | | `x-pulse-delivery-id` | The delivery identifier β€” your idempotency key | | `x-pulse-event` | The event name, e.g. `successful.sale` | | `Content-Type` | `application/json; charset=utf-8` | | `User-Agent` | `Pulse \| Chariow +https://chariow.com` | Full signing contract, verification snippets and troubleshooting: [Pulse Security](/en/guides/pulse-security). ## Setting Up Pulses You can configure Pulses in several ways: ### Via Store Dashboard 1. Go to **Automation** β†’ **Pulses** 2. Click **Add Pulse** 3. Enter your webhook endpoint URL (must be HTTPS) 4. Select the events you want to receive 5. Optionally select specific products (leave empty for all products) 6. Save your Pulse Your Pulse endpoint must be accessible via HTTPS. HTTP endpoints are not supported for security reasons. ## Pulse Events Pulses support the following events. When an event fires, the webhook payload contains an `event` field with the event value. ### Sale Events | Event Value | Label | Description | | ----------------- | --------------- | --------------------------------- | | `successful.sale` | Successful Sale | Triggers when a sale is completed | | `abandoned.sale` | Abandoned Sale | Triggers when a sale is abandoned | | `failed.sale` | Failed Sale | Triggers when a sale fails | ### License Events | Event Value | Label | Description | | ------------------------ | ---------------------- | ------------------------------------------------------ | | `license.activated` | License Activated | Triggers when a license is activated | | `license.expired` | License Expired | Triggers when a license expires | | `license.issued` | License Issued | Triggers when a license is issued to a customer | | `license.nearing_expiry` | License Nearing Expiry | Triggers when a license is approaching its expiry date | | `license.revoked` | License Revoked | Triggers when a license is revoked | ### Affiliate Events | Event Value | Label | Description | | ------------------ | ---------------- | ---------------------------------------------- | | `affiliate.joined` | Affiliate Joined | Triggers when a new affiliate joins your store | You can configure multiple events for a single Pulse URL. When any of the selected events occur, Chariow will send a webhook notification to your endpoint with the corresponding event value. ## Pulse Payload When a configured event occurs, Chariow sends an HTTP POST request to your webhook URL with event data in the request body. The exact payload structure depends on the event type. Pulse payloads contain comprehensive event data, including details about the entity (sale, license, etc.), customer information, product details, and store context. The payload structure varies by event type to provide relevant information for each event. ### Successful Sale Example When a successful sale occurs, Chariow sends a webhook with the `successful.sale` event: ```json theme={null} { "event": "successful.sale", "sale": { "id": "sal_xyz789abc", "amount": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "original_amount": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "discount_amount": { "value": 0, "formatted": "$0.00", "short": "0", "currency": "USD" }, "settlement": { "amount": { "value": 89.10, "formatted": "$89.10", "short": "89", "currency": "USD" }, "due_at": "2025-01-22T10:30:00+00:00", "done_at": null, "service_fee": { "value": 4.95, "formatted": "$4.95", "short": "5", "currency": "USD" }, "payment_fee": { "value": 4.95, "formatted": "$4.95", "short": "5", "currency": "USD" }, "fee": { "value": 9.90, "formatted": "$9.90", "short": "10", "currency": "USD" } }, "status": "completed", "created_at": "2025-01-15T10:30:00+00:00", "custom_fields": null, "custom_metadata": {"order_ref": "ORD-123"}, "completed_at": "2025-01-15T10:32:00+00:00", "abandoned_at": null, "failed_at": null }, "product": { "id": "prd_def456", "name": "Premium Course", "url": "https://mystore.mychariow.com/p/premium-course", "price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" } }, "customer": { "id": "cus_abc123", "name": "John Doe", "first_name": "John", "last_name": "Doe", "email": "customer@example.com", "phone": "+1234567890", "country": "US" }, "affiliate": null, "store": { "id": "str_xyz789", "name": "My Digital Store", "url": "https://mystore.mychariow.com" }, "checkout": { "url": "https://mystore.mychariow.com/checkout/sal_xyz789abc" } } ``` ### License Activated Example When a license is activated, Chariow sends a webhook with the `license.activated` event: ```json theme={null} { "event": "license.activated", "license": { "id": "lic_ghi789def", "key": "ABCD-1234-EFGH-5678", "status": "active", "source_type": "sale", "activation_count": 1, "max_activations": 5, "activated_at": "2025-01-15T10:35:00+00:00", "expires_at": "2026-01-15T10:35:00+00:00", "expired_at": null, "revoked_at": null, "created_at": "2025-01-15T10:30:00+00:00" }, "product": { "id": "prd_def456", "name": "Pro Software License", "url": "https://mystore.mychariow.com/p/pro-software" }, "customer": { "id": "cus_abc123", "name": "John Doe", "first_name": "John", "last_name": "Doe", "email": "customer@example.com", "phone": "+1234567890", "country": "US" }, "store": { "id": "str_xyz789", "name": "My Digital Store", "url": "https://mystore.mychariow.com" } } ``` ### License Nearing Expiry Example Chariow scans licenses daily and sends the `license.nearing_expiry` event once per license, as soon as it enters the seven-day window before its `expires_at` date. Use it to prompt a renewal before the customer loses access. `days_until_expiry` is only present on this event. It is rounded up, so a license lapsing in a few hours reports `1`. A license is announced once and only once, even if the scan runs again before it expires. ```json theme={null} { "event": "license.nearing_expiry", "days_until_expiry": 7, "license": { "id": "lic_ghi789def", "key": "ABCD-1234-EFGH-5678", "status": "active", "source_type": "sale", "activation_count": 1, "max_activations": 5, "activated_at": "2025-01-15T10:35:00+00:00", "expires_at": "2026-01-15T10:35:00+00:00", "expired_at": null, "revoked_at": null, "created_at": "2025-01-15T10:30:00+00:00" }, "product": { "id": "prd_def456", "name": "Pro Software License", "url": "https://mystore.mychariow.com/p/pro-software" }, "customer": { "id": "cus_abc123", "name": "John Doe", "first_name": "John", "last_name": "Doe", "email": "customer@example.com", "phone": "+1234567890", "country": "US" }, "store": { "id": "str_xyz789", "name": "My Digital Store", "url": "https://mystore.mychariow.com" } } ``` ### Affiliate Joined Example When a new affiliate joins your store, Chariow sends a webhook with the `affiliate.joined` event: ```json theme={null} { "event": "affiliate.joined", "affiliate": { "id": "saff_xyz789abc", "account": { "id": "aff_abc123def", "code": "CREATOR123", "pseudo": "creative_studio", "email": "creator@example.com", "first_name": "John", "last_name": "Doe", "name": "John Doe", "country": { "code": "US", "name": "United States" }, "phone": { "country_code": "US", "formatted": "+1 234 567 890" } }, "source": "invitation", "status": "active", "joined_at": "2025-01-15T10:40:00+00:00" }, "store": { "id": "str_xyz789", "name": "My Digital Store", "url": "https://mystore.mychariow.com" } } ``` The exact payload structure may include additional fields depending on the event type and context. Always check the actual payload received at your endpoint for the complete data structure. ## Handling Pulses ### Basic Example (Node.js/Express) ```javascript theme={null} const express = require('express'); const app = express(); app.post('/webhooks/chariow', express.json(), (req, res) => { const payload = req.body; const event = payload.event; switch (event) { case 'successful.sale': handleSuccessfulSale(payload.sale, payload.product, payload.customer, payload.store); break; case 'abandoned.sale': handleAbandonedSale(payload.sale, payload.store); break; case 'failed.sale': handleFailedSale(payload.sale, payload.store); break; case 'license.activated': handleLicenseActivated(payload.license, payload.product, payload.customer); break; case 'license.expired': handleLicenseExpired(payload.license, payload.customer); break; case 'license.issued': handleLicenseIssued(payload.license, payload.product, payload.customer); break; case 'license.nearing_expiry': handleLicenseNearingExpiry(payload.license, payload.customer, payload.days_until_expiry); break; case 'license.revoked': handleLicenseRevoked(payload.license, payload.customer); break; case 'affiliate.joined': handleAffiliateJoined(payload.affiliate, payload.store); break; default: console.log(`Unhandled event type: ${event}`); } // Always return 200 to acknowledge receipt res.status(200).send('OK'); }); function handleSuccessfulSale(sale, store) { console.log(`Sale completed: ${sale.id} in store ${store.name}`); // Add customer to CRM, send confirmation email, grant access, etc. } function handleLicenseActivated(license, customer) { console.log(`License activated: ${license.key} for ${customer.email}`); // Log activation, update internal records, notify customer, etc. } function handleLicenseNearingExpiry(license, customer, daysUntilExpiry) { console.log(`License ${license.key} expires in ${daysUntilExpiry} day(s)`); // Send a renewal reminder, create a renewal offer, flag the account, etc. } function handleAffiliateJoined(affiliate, store) { console.log(`New affiliate joined: ${affiliate.account.code} in store ${store.name}`); // Send welcome email, create affiliate dashboard access, notify team, etc. } app.listen(3000, () => console.log('Webhook server running on port 3000')); ``` ### PHP Example ```php theme={null} After the 5th failed attempt on a single delivery, **the Pulse is disabled automatically** and the store owner, admins and marketing team members receive an email. You must re-enable it from the dashboard before it fires again β€” and before you can replay its deliveries. Ensure your Pulse endpoint responds quickly (within 30 seconds). Long-running processes should be handled asynchronously. ## Delivery History and Replay Every attempt is recorded, so you can see whether Chariow ever called your endpoint and what it answered. Open **Automations β†’ Pulses β†’ select your Pulse β†’ Deliveries tab**. The table lists each delivery with: | Field | Meaning | | ------------------ | ------------------------------------------------------ | | `status` | `pending`, `succeeded` or `failed` | | `http_status_code` | The status code your endpoint returned | | `attempts` | How many attempts have been made so far | | `payload` | The exact body that was sent | | `response_body` | Your endpoint's response, truncated | | `is_replay` | Whether this delivery was triggered by a manual replay | `pending` means queued or mid-retry, not failed. A delivery only becomes `failed` once all 5 attempts are exhausted. Clicking a row opens the sent payload, the response and the delivery id. **Replay** re-sends a stored payload to the same endpoint without reprocessing the original sale or licence event β€” useful when your endpoint was down, or while you are debugging signature verification. A replay is a new delivery with a new `x-pulse-delivery-id`, so it passes your idempotency check and will be processed again. That is intentional. ## Best Practices Return a 200 response immediately, then process the Pulse asynchronously to avoid timeouts: ```javascript theme={null} app.post('/webhooks/chariow', (req, res) => { // Acknowledge receipt immediately res.status(200).send('OK'); // Process asynchronously (e.g., queue job, background worker) queuePulseProcessing(req.body); }); ``` Due to retry logic, the same delivery may reach you more than once. Deduplicate on the `x-pulse-delivery-id` header, which is stable across every attempt of a delivery: ```javascript theme={null} const processedDeliveries = new Set(); async function processPulse(deliveryId, payload) { if (processedDeliveries.has(deliveryId)) { console.log('Duplicate delivery, skipping'); return; } processedDeliveries.add(deliveryId); // Process the pulse... } ``` Do not deduplicate on the entity id inside the payload: one sale legitimately produces one delivery per subscribed Pulse. Never act on a payload before checking `x-chariow-signature`. See [Pulse Security](/en/guides/pulse-security) for the full contract and ready-to-use snippets. Always use HTTPS for your Pulse endpoint to ensure data is encrypted in transit. Chariow will reject HTTP endpoints for security reasons. Check the **Deliveries** tab of your Pulse regularly. It shows the status code and response body of every attempt, so you can diagnose failures without adding logging on your side. For high-volume stores, consider creating separate Pulses for different products to make processing more efficient and organised. ## Testing Pulses Use the Pulse testing feature in your dashboard: 1. Go to **Automations** β†’ **Pulses** 2. Click on your Pulse 3. Click **Send test pulse** 4. Check your endpoint received the test payload A test event is signed exactly like a real one, but it carries no `x-pulse-delivery-id` header (no delivery record is created for it) and its payload contains an extra `note` field. To validate your integration against a real payload, replay a delivery from the **Deliveries** tab instead. For local development, use a service like [ngrok](https://ngrok.com) to expose your local server to the internet. ## Managing Pulses via API You can programmatically manage your Pulses using the Chariow Public API: ### List All Pulses ```bash theme={null} curl -X GET "https://api.chariow.com/v1/pulses" \ -H "Authorization: Bearer YOUR_API_KEY" ``` #### Example List Response ```json theme={null} { "data": [ { "id": "pls_abc123xyz", "url": "https://example.com/webhooks/chariow", "is_enabled": true, "source": { "value": "manual", "label": "Manual", "description": "Created manually" }, "triggers": [ { "value": "successful_sale", "label": "Successful Sale", "description": "Triggers when a sale is completed" } ], "products": [ { "id": "prd_def456", "name": "Premium Course", "type": "course", "pictures": { "thumbnail": "https://cdn.chariow.com/products/thumb.jpg", "cover": "https://cdn.chariow.com/products/cover.jpg" }, "category": { "value": "education_and_learning", "label": "Education and Learning" }, "pricing": { "type": "one_time", "price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "effective": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" } }, "bundle": null, "metadata": null } ], "store": { "id": "str_xyz789", "name": "My Digital Store", "logo_url": "https://cdn.chariow.com/stores/xyz789/logo.png", "url": "https://mystore.mychariow.com" }, "is_editable": true, "created_at": "2025-01-10T08:00:00+00:00", "updated_at": "2025-01-10T08:00:00+00:00" } ], "pagination": { "next_cursor": null, "prev_cursor": null, "has_more": false } } ``` ### Get a Specific Pulse ```bash theme={null} curl -X GET "https://api.chariow.com/v1/pulses/pls_abc123xyz" \ -H "Authorization: Bearer YOUR_API_KEY" ``` The response for a single pulse uses the same structure as each item in the list response. ### Filter Pulses You can filter pulses by URL or event type using the search parameter: ```bash theme={null} curl -X GET "https://api.chariow.com/v1/pulses?search=successful_sale" \ -H "Authorization: Bearer YOUR_API_KEY" ``` For complete API documentation, see the [List Pulses](/api-reference/pulses/list-pulses) and [Get Pulse](/api-reference/pulses/get-pulse) endpoints. ## Related Resources Verify signatures and deduplicate retries View detailed API documentation Get a specific pulse via API Learn about sale events Learn about license events Learn about affiliate events # SaaS License Integration Source: https://chariow.dev/en/guides/saas-license-integration Complete guide to implementing license-based paywalls in your SaaS application using Chariow API with AI coding tools This guide shows you how to use Chariow's License API to implement a paywall in your SaaS application. Whether you're building with Lovable, Cursor, Bolt, or any AI coding assistant, this guide provides everything you need including ready-to-use AI prompts. This guide assumes you have a Chariow store with a **License** type product created. If you haven't set one up yet, [create your license product](https://app.chariow.com) first. ## What is a License Paywall? A license paywall restricts access to your SaaS application (or specific features) until the user provides a valid license key. This is ideal for: * **Desktop applications**: Electron apps, native software, CLI tools * **Web applications**: SaaS platforms, admin dashboards, premium tools * **Mobile apps**: iOS/Android applications with premium features * **API access**: Gating API usage based on license validity ### How It Works Customer buys your license product on Chariow. A unique license key is automatically generated (e.g., `ABC-123-XYZ-789`). In your application, the customer enters their license key in a settings or activation screen. Your application calls the Chariow API to validate the license key and check its status. Based on the API response (`is_active`, `is_expired`), your app grants or denies access. For device-limited licenses, activate the license to track and limit device usage. ## API Endpoints You'll Need | Endpoint | Method | Purpose | | ----------------------------- | ------ | --------------------------------------- | | `/v1/licenses/{key}` | GET | Validate a license key and check status | | `/v1/licenses/{key}/activate` | POST | Activate license on a device | Your API key (`sk_live_...`) must be kept **server-side only**. Never expose it in client-side code. ## Architecture Patterns ### Pattern 1: Server-Side Validation (Recommended) Your backend validates licenses and controls access. Best for web applications. Server-side license validation flow ### Pattern 2: Serverless/Edge Validation Validate licenses at the edge using serverless functions. Good for static sites. Serverless license validation flow ### Pattern 3: Desktop App with Periodic Validation Desktop apps validate on startup and periodically. Includes offline grace period. Desktop app license validation flow with local cache ## Implementation Guide ### Step 1: Create an API Route for License Validation Your backend should expose an endpoint that your frontend calls: ```javascript theme={null} // /api/validate-license.js (Next.js API route) export default async function handler(req, res) { const { licenseKey } = req.body; if (!licenseKey) { return res.status(400).json({ valid: false, error: 'License key required' }); } try { const response = await fetch( `https://api.chariow.com/v1/licenses/${encodeURIComponent(licenseKey)}`, { headers: { 'Authorization': `Bearer ${process.env.CHARIOW_API_KEY}` } } ); if (!response.ok) { return res.status(200).json({ valid: false, error: 'Invalid license key' }); } const { data } = await response.json(); // Check license validity if (!data.is_active) { return res.status(200).json({ valid: false, error: 'License is not active' }); } if (data.is_expired) { return res.status(200).json({ valid: false, error: 'License has expired' }); } // License is valid return res.status(200).json({ valid: true, license: { status: data.status, expiresAt: data.expires_at, activationsRemaining: data.activations?.remaining } }); } catch (error) { console.error('License validation error:', error); return res.status(500).json({ valid: false, error: 'Validation failed' }); } } ``` ### Step 2: Create the License Entry Component ```jsx theme={null} // components/LicenseGate.jsx import { useState, useEffect } from 'react'; export function LicenseGate({ children }) { const [isValidated, setIsValidated] = useState(false); const [isLoading, setIsLoading] = useState(true); const [licenseKey, setLicenseKey] = useState(''); const [error, setError] = useState(''); // Check for stored license on mount useEffect(() => { const storedKey = localStorage.getItem('license_key'); if (storedKey) { validateLicense(storedKey); } else { setIsLoading(false); } }, []); const validateLicense = async (key) => { setIsLoading(true); setError(''); try { const response = await fetch('/api/validate-license', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ licenseKey: key }) }); const data = await response.json(); if (data.valid) { localStorage.setItem('license_key', key); setIsValidated(true); } else { setError(data.error || 'Invalid license'); localStorage.removeItem('license_key'); } } catch (err) { setError('Failed to validate license'); } finally { setIsLoading(false); } }; const handleSubmit = (e) => { e.preventDefault(); validateLicense(licenseKey); }; if (isLoading) { return
Validating license...
; } if (isValidated) { return children; } return (

Enter Your License Key

Please enter your license key to access the application.

setLicenseKey(e.target.value)} placeholder="ABC-123-XYZ-789" />
{error &&

{error}

}

Don't have a license? Purchase one here

); } ``` ### Step 3: Wrap Your App with the License Gate ```jsx theme={null} // App.jsx import { LicenseGate } from './components/LicenseGate'; import { Dashboard } from './components/Dashboard'; function App() { return ( ); } ``` ## Device Activation (Optional) If your app tracks its own devices, skip activation entirely: set `requires_activation` to `false` in the product's licence settings and Chariow issues every licence already active, with its validity period starting at purchase. Your app then only ever calls `GET /v1/licenses/{licenseKey}` to validate. See [Licences that need no activation](/en/guides/licenses#licences-that-need-no-activation). If your license has limited activations and you want Chariow to count the seats, activate on the device: ```javascript theme={null} // /api/activate-license.js export default async function handler(req, res) { const { licenseKey, deviceId } = req.body; const response = await fetch( `https://api.chariow.com/v1/licenses/${encodeURIComponent(licenseKey)}/activate`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.CHARIOW_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ device_identifier: deviceId }) } ); const data = await response.json(); if (!response.ok) { return res.status(400).json({ success: false, error: data.message }); } return res.status(200).json({ success: true, activationsRemaining: data.data.activations?.remaining }); } ``` ## AI Prompts for Implementation Use these prompts with Lovable, Cursor, Bolt, or any AI coding assistant to implement the license paywall quickly. ### Prompt 1: Basic License Paywall (React + Next.js) ```text theme={null} Create a license paywall system for my Next.js application with these requirements: 1. Create an API route at /api/validate-license that: - Accepts POST requests with { licenseKey: string } - Calls Chariow API: GET https://api.chariow.com/v1/licenses/{licenseKey} - Uses Bearer token authentication with CHARIOW_API_KEY environment variable - Returns { valid: true/false, error?: string, license?: object } - Check is_active and is_expired fields from the response 2. Create a LicenseGate component that: - Shows a license key input form when not validated - Stores valid license key in localStorage - Checks localStorage on mount to auto-validate returning users - Renders children only when license is valid - Shows loading state during validation - Displays error messages for invalid licenses 3. Style the license form with Tailwind CSS: - Centre the form on the page - Use clean, professional styling - Include a link to purchase a license Environment variable needed: CHARIOW_API_KEY (store API key) ``` ### Prompt 2: License Paywall with Device Activation ```text theme={null} Extend the license paywall system with device activation: 1. Generate a unique device identifier: - For web apps: combine user agent + screen resolution + timezone - Hash the result to create a consistent device ID - Store the device ID in localStorage 2. Create /api/activate-license endpoint: - Accepts POST with { licenseKey, deviceId } - Calls Chariow API: POST https://api.chariow.com/v1/licenses/{key}/activate - Body: { device_identifier: deviceId } - Handle "Activation limit reached" error gracefully 3. Update LicenseGate component: - After validating license, call activate endpoint - Show remaining activations count to user - Handle activation errors with user-friendly messages 4. Add a "Manage Devices" section showing: - Current activation count - Maximum activations allowed - Option to deactivate current device ``` ### Prompt 3: Feature-Based Licensing ```text theme={null} Implement feature-gated licensing where different license tiers unlock different features: 1. Create a useLicense hook that: - Validates license on app load - Returns { isValid, tier, features, expiresAt } - Tier is determined by the product name or metadata from Chariow 2. Create a FeatureGate component: - Accepts requiredTier prop (e.g., "pro", "enterprise") - Shows upgrade prompt if user's tier is insufficient - Renders children if tier requirement is met 3. Example usage: 4. Create an upgrade modal that: - Shows current vs required tier - Links to Chariow checkout for the upgrade product - Includes feature comparison ``` ### Prompt 4: Offline-Capable License Validation ```text theme={null} Add offline support to the license system: 1. Cache license validation result locally: - Store validation timestamp - Store license expiry date - Store full license object 2. Implement validation logic: - If online: validate with Chariow API - If offline: check cached validation (valid for 7 days) - If cache expired: show offline warning but allow limited access 3. Add periodic re-validation: - Re-validate every 24 hours when online - Update cache on successful validation - Handle network errors gracefully 4. Show license status indicator: - Green: validated online today - Yellow: using cached validation - Red: cache expired, needs connection ``` ### Prompt 5: Complete Electron App Implementation ```text theme={null} Create a license system for an Electron desktop app: 1. Main process license manager: - Store license in electron-store (encrypted) - Validate on app launch - Re-validate every 24 hours - Send validation status to renderer via IPC 2. Preload script: - Expose safe license APIs to renderer - licenseAPI.validate(key) - licenseAPI.getStatus() - licenseAPI.activate() 3. Renderer license UI: - License entry screen on first launch - License status in settings - "Manage Activations" option 4. Device identifier: - Use machine-id package for consistent device ID - Pass to Chariow activation endpoint 5. Offline handling: - Cache validation for 7 days - Show "Offline Mode" indicator - Block app after cache expires ``` ### Prompt 6: Supabase Integration ```text theme={null} Integrate license validation with Supabase authentication: 1. Create a Supabase edge function at /validate-license: - Verify user is authenticated - Store validated licenses in a 'user_licenses' table - Link license to user ID 2. Database schema: CREATE TABLE user_licenses ( id UUID PRIMARY KEY, user_id UUID REFERENCES auth.users, license_key TEXT NOT NULL, validated_at TIMESTAMP, expires_at TIMESTAMP, status TEXT ); 3. React hook useUserLicense: - Check if current user has valid license - Sync license status on login - Handle multi-device scenarios 4. Row Level Security: - Users can only see their own licenses - Enable realtime subscriptions for status updates ``` ## Security Best Practices Never expose your Chariow API key in client-side code. Always validate licenses through your backend. ### Do's * **Store API keys server-side** in environment variables * **Validate on the backend** before granting access * **Cache validation results** to reduce API calls * **Implement rate limiting** on your validation endpoint * **Use HTTPS** for all API communications * **Log validation attempts** for security auditing ### Don'ts * **Don't trust client-side validation alone** - it can be bypassed * **Don't store the full license object** in accessible localStorage * **Don't skip validation** for "trusted" users * **Don't hardcode license keys** in your application * **Don't expose detailed error messages** that could help attackers ## Testing Your Integration ### Test Scenarios | Scenario | Expected Behaviour | | ------------------------ | ------------------------------------------------ | | Valid, active license | Access granted | | Invalid license key | Show "Invalid license" error | | Expired license | Show "License expired" message with renewal link | | Revoked license | Show "License revoked" - contact support | | Network error | Show cached result or offline warning | | Activation limit reached | Show "Too many devices" with management options | ### Test License Keys During development, create test products in your Chariow store with: * Free test licenses for development * Short expiry periods to test expiration handling * Limited activations to test device limits ## Complete Example: Next.js App Router Here's a complete implementation using Next.js App Router: ```typescript app/api/license/validate/route.ts theme={null} import { NextResponse } from 'next/server'; export async function POST(request: Request) { const { licenseKey } = await request.json(); if (!licenseKey) { return NextResponse.json( { valid: false, error: 'License key required' }, { status: 400 } ); } try { const response = await fetch( `https://api.chariow.com/v1/licenses/${encodeURIComponent(licenseKey)}`, { headers: { 'Authorization': `Bearer ${process.env.CHARIOW_API_KEY}` } } ); if (!response.ok) { return NextResponse.json({ valid: false, error: 'Invalid license key' }); } const { data } = await response.json(); if (!data.is_active) { return NextResponse.json({ valid: false, error: 'License is not active' }); } if (data.is_expired) { return NextResponse.json({ valid: false, error: 'License has expired' }); } return NextResponse.json({ valid: true, license: { status: data.status, expiresAt: data.expires_at, activationsRemaining: data.activations?.remaining } }); } catch (error) { return NextResponse.json( { valid: false, error: 'Validation failed' }, { status: 500 } ); } } ``` ```typescript hooks/useLicense.ts theme={null} 'use client'; import { useState, useEffect, useCallback } from 'react'; interface LicenseState { isLoading: boolean; isValid: boolean; error: string | null; expiresAt: string | null; } export function useLicense() { const [state, setState] = useState({ isLoading: true, isValid: false, error: null, expiresAt: null }); const validate = useCallback(async (key: string): Promise => { setState(s => ({ ...s, isLoading: true, error: null })); try { const res = await fetch('/api/license/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ licenseKey: key }) }); const data = await res.json(); if (data.valid) { localStorage.setItem('license_key', key); setState({ isLoading: false, isValid: true, error: null, expiresAt: data.license?.expiresAt }); return true; } else { localStorage.removeItem('license_key'); setState({ isLoading: false, isValid: false, error: data.error, expiresAt: null }); return false; } } catch { setState(s => ({ ...s, isLoading: false, error: 'Validation failed' })); return false; } }, []); const logout = useCallback(() => { localStorage.removeItem('license_key'); setState({ isLoading: false, isValid: false, error: null, expiresAt: null }); }, []); useEffect(() => { const storedKey = localStorage.getItem('license_key'); if (storedKey) { validate(storedKey); } else { setState(s => ({ ...s, isLoading: false })); } }, [validate]); return { ...state, validate, logout }; } ``` ```tsx components/LicenseGate.tsx theme={null} 'use client'; import { useState } from 'react'; import { useLicense } from '@/hooks/useLicense'; export function LicenseGate({ children }: { children: React.ReactNode }) { const { isLoading, isValid, error, validate } = useLicense(); const [keyInput, setKeyInput] = useState(''); if (isLoading) { return (

Validating license...

); } if (isValid) { return <>{children}; } return (

Enter Your License Key

Please enter your license key to access the application.

{ e.preventDefault(); validate(keyInput); }} className="space-y-4" > setKeyInput(e.target.value)} placeholder="ABC-123-XYZ-789" className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" />
{error && (

{error}

)}

Don't have a license?{' '} Purchase one here

); } ```
## Next Steps Learn more about license management View the complete License API reference Get notified when licenses are purchased Security and integration best practices # Sales Source: https://chariow.dev/en/guides/sales Learn how to retrieve and manage sales data via the Chariow API Sales represent purchase transactions in your store, whether completed, pending, abandoned, failed, or settled. Each sale contains comprehensive information about the product, customer, payment details, applied discounts, shipping information, and post-purchase access. ## Understanding Sales The Chariow Sales API provides programmatic access to all transaction data in your store. You can: * Retrieve a list of all sales with filtering options * Get detailed information about specific sales * Track payment status and settlement details * Access customer purchase history * Monitor download statistics * View applied discounts and marketing campaigns ## Sale Object The detailed sale object (returned by the single sale endpoint) contains comprehensive purchase information with the following structure: ```json theme={null} { "id": "sal_abc123xyz", "status": "completed", "channel": { "value": "store", "label": "Store", "description": "Sale made through the store checkout" }, "original_amount": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "discount_amount": { "value": 19.80, "formatted": "$19.80", "short": "20", "currency": "USD" }, "settlement": { "amount": { "value": 75.24, "formatted": "$75.24", "short": "75", "currency": "USD" }, "due_at": "2025-02-01T00:00:00+00:00", "done_at": "2025-02-01T10:15:00+00:00", "service_fee": { "value": 3.96, "formatted": "$3.96", "short": "4", "currency": "USD" } }, "download": { "total": 3, "last_at": "2025-01-20T14:30:00+00:00" }, "invoice_download_url": "https://api.chariow.com/invoices/sal_abc123xyz.pdf", "payment": { "status": "success", "transaction_id": "txn_moneroo_xyz789", "gateway": "moneroo", "method": { "name": "Credit/Debit Card", "icon_url": "https://assets.cdn.moneroo.io/icons/circle/card.svg" }, "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "fee": { "value": 2.37, "formatted": "$2.37", "short": "2", "currency": "USD" }, "fee_rate": "3%", "interchange": { "rate": "1.5%", "fee": { "value": 1.19, "formatted": "$1.19", "short": "1", "currency": "USD" } }, "exchange_rate": { "value": 1.0, "formatted": "$1.00", "short": "1", "currency": "USD" }, "failure_error": null }, "shipping": { "address": "123 Main Street", "city": "New York", "state": "NY", "country": { "name": "United States", "code": "US", "alpha_3_code": "USA", "dial_code": "+1", "currency": "USD", "flag": "πŸ‡ΊπŸ‡Έ" }, "zip": "10001" }, "context": { "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "ip_address": "203.0.113.42", "country": { "name": "United States", "code": "US", "alpha_3_code": "USA", "dial_code": "+1", "currency": "USD", "flag": "πŸ‡ΊπŸ‡Έ" }, "device_type": "desktop", "locale": "en_US" }, "custom_fields_values": null, "campaign": { "id": "cmp_pqr678", "name": "Black Friday Campaign" }, "rating": { "id": "rat_mno345", "is_thumbs_up": true, "comment": "Excellent product!", "created_at": "2025-01-18T09:00:00+00:00" }, "store": { "id": "str_xyz789", "name": "My Digital Store", "logo_url": "https://cdn.chariow.com/stores/xyz789/logo.png", "url": "https://mystore.mychariow.com" }, "product": { "id": "prd_def456", "name": "Premium Course", "type": "course", "pictures": { "thumbnail": "https://cdn.chariow.com/products/thumb.jpg", "cover": "https://cdn.chariow.com/products/cover.jpg" }, "category": { "value": "education_and_learning", "label": "Education and Learning" }, "pricing": { "type": "one_time", "price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "effective": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" } }, "bundle": null, "metadata": null }, "customer": { "id": "cus_ghi789", "name": "John Doe", "first_name": "John", "last_name": "Doe", "email": "customer@example.com", "avatar_url": null }, "discount": { "id": "dis_jkl012", "name": "Save 20%", "code": "SAVE20" }, "store_affiliate": null, "affiliate_commission": null, "is_reconciled": true, "last_reconciled_at": "2025-02-01T10:15:00+00:00", "failed_at": null, "awaiting_payment_at": "2025-01-15T10:30:00+00:00", "abandoned_at": null, "completed_at": "2025-01-15T10:32:00+00:00", "created_at": "2025-01-15T10:30:00+00:00", "updated_at": "2025-01-15T10:32:00+00:00" } ``` The list endpoint returns a simplified version of the sale object (using `SalePublicResource`), whilst the single sale endpoint returns the full detailed object (using `SaleResource`). See the [Listing Sales](#listing-sales) and [Getting a Single Sale](#getting-a-single-sale) sections for the differences. ## Sale Statuses Sales can have the following statuses throughout their lifecycle: | Status | Description | | ------------------ | ------------------------------------------------------ | | `awaiting_payment` | Checkout initiated, waiting for payment confirmation | | `completed` | Payment successful, product access granted to customer | | `failed` | Payment failed or was declined | | `abandoned` | Customer abandoned the checkout process | | `settled` | Funds have been settled to the merchant account | ## Payment Statuses The payment status tracks the payment gateway transaction separately from the sale status: | Payment Status | Description | | -------------- | ------------------------------------------- | | `initiated` | Payment process has started | | `pending` | Payment is being processed by the gateway | | `success` | Payment was successfully processed | | `failed` | Payment failed or was declined | | `cancelled` | Payment was cancelled by customer or system | ## Sales Channels Sales can originate from different channels: | Channel | Description | | ----------- | ---------------------------------------- | | `store` | Direct sale through your store checkout | | `affiliate` | Sale made through an affiliate link | | `discover` | Sale from Chariow Discover marketplace | | `widget` | Sale through an embedded checkout widget | | `api` | Sale created via the Public API | ## Listing Sales Retrieve all sales for your store: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/sales" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/sales', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const result = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/sales', headers={'Authorization': 'Bearer sk_live_your_api_key'} ) sales = response.json()['data'] ``` ### Query Parameters You can filter and paginate sales using the following parameters: | Parameter | Type | Description | | ------------- | ------- | ----------------------------------------------------------------------------------------- | | `per_page` | integer | Number of sales per page (max 100, default 15) | | `cursor` | string | Cursor for pagination (from `next_cursor` or `prev_cursor`) | | `status` | string | Filter by sale status (`awaiting_payment`, `completed`, `failed`, `abandoned`, `settled`) | | `customer_id` | string | Filter by customer public ID (e.g., `cus_abc123xyz`) | | `search` | string | Search by sale reference or customer email | | `start_date` | string | Filter sales from this date (format: `Y-m-d`, e.g., `2025-01-01`) | | `end_date` | string | Filter sales until this date (format: `Y-m-d`, e.g., `2025-01-31`) | ### Filtering by Status ```bash theme={null} # Get only completed sales curl -X GET "https://api.chariow.com/v1/sales?status=completed" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ### Filtering by Customer ```bash theme={null} # Get sales for a specific customer curl -X GET "https://api.chariow.com/v1/sales?customer_id=cus_abc123" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ### Filtering by Date Range ```bash theme={null} # Get sales from January 2025 curl -X GET "https://api.chariow.com/v1/sales?start_date=2025-01-01&end_date=2025-01-31" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ### Searching Sales ```bash theme={null} # Search by customer email or sale reference curl -X GET "https://api.chariow.com/v1/sales?search=customer@example.com" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ### Pagination The API uses cursor-based pagination. Use the `next_cursor` from the response to fetch the next page: ```javascript theme={null} let cursor = null; let allSales = []; do { const url = cursor ? `https://api.chariow.com/v1/sales?cursor=${cursor}` : 'https://api.chariow.com/v1/sales'; const response = await fetch(url, { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const result = await response.json(); allSales = [...allSales, ...result.data]; cursor = result.pagination.next_cursor; } while (cursor); ``` ### Example Response The list endpoint returns a simplified sale object per item: ```json theme={null} { "data": [ { "id": "sal_abc123xyz", "status": "completed", "original_amount": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "discount_amount": { "value": 19.80, "formatted": "$19.80", "short": "20", "currency": "USD" }, "payment": { "amount": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" }, "status": "success", "exchange_rate": { "value": 1.0, "formatted": "$1.00", "short": "1", "currency": "USD" }, "failure_error": null }, "shipping": { "address": "123 Main Street", "city": "New York", "state": "NY", "country": "US", "zip": "10001" }, "invoice_download_url": "https://api.chariow.com/invoices/sal_abc123xyz.pdf", "created_at": "2025-01-15T10:30:00+00:00", "completed_at": "2025-01-15T10:32:00+00:00", "store": { "id": "str_ghi789", "name": "My Digital Store", "logo_url": "https://cdn.chariow.com/stores/ghi789/logo.png", "url": "https://mystore.mychariow.com" }, "product": { "id": "prd_def456", "name": "Premium Course", "type": "course", "pictures": { "thumbnail": "https://cdn.chariow.com/products/thumb.jpg", "cover": "https://cdn.chariow.com/products/cover.jpg" }, "category": { "value": "education_and_learning", "label": "Education and Learning" }, "pricing": { "type": "one_time", "price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" }, "effective": { "value": 79.20, "formatted": "$79.20", "short": "79", "currency": "USD" } }, "bundle": null, "metadata": null }, "customer": { "id": "cus_xyz789", "name": "John Doe", "first_name": "John", "last_name": "Doe", "email": "customer@example.com", "avatar_url": null }, "discount": { "id": "dis_jkl012", "name": "Save 20%", "code": "SAVE20" }, "rate": { "id": "rat_mno345", "is_thumbs_up": true, "comment": "Excellent product!", "created_at": "2025-01-18T09:00:00+00:00" }, "fulfillment": null } ], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true } } ``` ## Getting a Single Sale Retrieve a specific sale by its public ID: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/sales/sal_abc123" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/sales/sal_abc123', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }); const result = await response.json(); ``` ## Post-Purchase Fulfillment Completed sales can include fulfillment data containing deliverables based on the product type. The `fulfillment` field is present in the list endpoint response when populated: ```json theme={null} { "fulfillment": { "files": [ { "id": "fil_abc123", "name": "course-materials.zip", "size": 15728640, "type": "application/zip", "download_url": "https://cdn.chariow.com/downloads/...", "expires_at": "2025-01-20T10:30:00+00:00" } ], "licences": [ { "id": "lic_def456", "licence_key": "ABC-123-XYZ-789", "status": "active", "activations": 2, "max_activations": 5, "expires_at": null } ], "instructions": "Thank you for your purchase! Here's how to get started with your course materials..." } } ``` ### File Downloads For products with downloadable files, the `files` array contains: * `id` - Unique file identifier * `name` - Original filename * `size` - File size in bytes * `type` - MIME type * `download_url` - Temporary signed download URL * `expires_at` - When the download link expires ### Licences For products with licence keys, the `licences` array contains: * `id` - Unique licence identifier * `licence_key` - The licence key string * `status` - Licence status (`active`, `inactive`, `expired`) * `activations` - Current number of activations * `max_activations` - Maximum allowed activations * `expires_at` - Expiry date (null for lifetime licences) ### Custom Instructions The `instructions` field contains custom post-purchase instructions configured for the product. ## Common Use Cases Calculate total revenue for a specific period: ```javascript theme={null} const response = await fetch( 'https://api.chariow.com/v1/sales?status=completed&start_date=2025-01-01&end_date=2025-01-31', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' }} ); const result = await response.json(); const totalRevenue = result.data.reduce( (sum, sale) => sum + sale.amount.value, 0 ); console.log(`Total revenue: ${totalRevenue}`); ``` Process orders that require physical shipping: ```javascript theme={null} // Get completed sales with shipping addresses const response = await fetch( 'https://api.chariow.com/v1/sales?status=completed', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' }} ); const result = await response.json(); const ordersToShip = result.data.filter( sale => sale.shipping && sale.shipping.address ); // Process each order ordersToShip.forEach(sale => { console.log(`Ship to: ${sale.shipping.address}, ${sale.shipping.city}`); }); ``` View a customer's complete purchase history: ```bash theme={null} curl -X GET "https://api.chariow.com/v1/sales?customer_id=cus_abc123xyz" \ -H "Authorization: Bearer sk_live_your_api_key" ``` This returns all sales for the specified customer, including: * Purchase dates and amounts * Products purchased * Applied discounts * Current access status Identify and process failed payments: ```javascript theme={null} const response = await fetch( 'https://api.chariow.com/v1/sales?status=failed', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' }} ); const result = await response.json(); // Review failed sales and payment error details result.data.forEach(sale => { if (sale.payment.failure_error) { console.log(`Failed sale ${sale.id}: ${sale.payment.failure_error.message}`); // Take action based on error code } }); ``` Analyse discount code usage and effectiveness: ```javascript theme={null} const response = await fetch( 'https://api.chariow.com/v1/sales?status=completed&start_date=2025-01-01', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' }} ); const result = await response.json(); // Group by discount code const discountStats = result.data .filter(sale => sale.discount) .reduce((acc, sale) => { const code = sale.discount.code; if (!acc[code]) { acc[code] = { uses: 0, revenue: 0, discountGiven: 0 }; } acc[code].uses++; acc[code].revenue += sale.amount.value; acc[code].discountGiven += sale.discount_amount.value; return acc; }, {}); console.log(discountStats); ``` ## Settlement Information For completed sales, the `settlement` object provides details about merchant payouts: ```json theme={null} { "settlement": { "amount": { "value": 75.24, "formatted": "$75.24", "short": "75", "currency": "USD" }, "due_at": "2025-02-01T00:00:00+00:00", "done_at": "2025-02-01T10:15:00+00:00", "service_fee": { "value": 3.96, "formatted": "$3.96", "short": "4", "currency": "USD" } } } ``` * `amount` - Net amount to be paid to merchant (after fees) * `due_at` - When settlement is scheduled * `done_at` - When settlement was completed (null if pending) * `service_fee` - Chariow platform service fee ## Download Tracking Monitor customer download activity: ```json theme={null} { "download": { "total": 3, "last_at": "2025-01-20T14:30:00+00:00" } } ``` This helps you: * Track product engagement * Identify customers who haven't accessed their purchase * Monitor for unusual download patterns ## Related Resources Learn how to create sales Get notified of new sales View the complete Sales API reference # Use Cases Source: https://chariow.dev/en/guides/use-cases Discover real-world integration scenarios for the Chariow API The Chariow API enables a wide range of integration scenarios for selling digital products. Here are five common use cases to help you get started. ## 1. Custom Storefront Integration Build a fully branded shopping experience on your own website while leveraging Chariow for payment processing and product delivery. ### Scenario You have an existing website or web application and want to sell digital products without redirecting customers to an external store. ### Implementation Fetch your product catalog using the [List Products](/api-reference/products/list-products) endpoint and display them on your website. Create a custom checkout form to collect customer details (name, email, phone). Call the [Checkout API](/api-reference/checkout/init-checkout) with customer data and redirect to the payment URL. Use a custom `redirect_url` to bring customers back to your thank-you page, and set up [Pulses](/en/guides/pulses) for reliable sale notifications. ### Code Example ```javascript theme={null} // Fetch products for display const products = await fetch('https://api.chariow.com/v1/products', { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } }).then(res => res.json()); // When customer submits checkout form async function handleCheckout(productId, customerData) { const response = await fetch('https://api.chariow.com/v1/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: productId, email: customerData.email, first_name: customerData.firstName, last_name: customerData.lastName, phone: { number: customerData.phone, country_code: customerData.countryCode }, redirect_url: 'https://yoursite.com/thank-you?sale={sale_id}' }) }); const result = await response.json(); if (result.data.step === 'payment') { window.location.href = result.data.payment.checkout_url; } } ``` *** ## 2. Software License Management Sell software with automated license key generation, validation, and activation management. ### Scenario You develop desktop or mobile software and need to protect it with license keys, control the number of installations, and manage activations. ### Implementation Create license-type products in Chariow with activation limits configured. When your software starts, call the [Get License](/api-reference/licenses/get-license) endpoint to check if the license is valid (verify `is_active`, `is_expired`, and `can_activate` fields). On first run, call [Activate License](/api-reference/licenses/activate-license) with a unique machine identifier. Skip this step entirely if the product has `requires_activation` set to `false` β€” its licences arrive already active. See [Licences that need no activation](/en/guides/licenses#licences-that-need-no-activation). Allow users to revoke licenses when switching devices using [Revoke License](/api-reference/licenses/revoke-license). ### Code Example ```python theme={null} import requests import hashlib import platform def get_machine_id(): """Generate a unique machine identifier""" machine_info = f"{platform.node()}-{platform.machine()}-{platform.processor()}" return hashlib.sha256(machine_info.encode()).hexdigest()[:32] def get_license(license_key): """Read the license via the Get License endpoint""" response = requests.get( f'https://api.chariow.com/v1/licenses/{license_key}', headers={'Authorization': 'Bearer sk_live_your_api_key'} ) response.raise_for_status() return response.json()['data'] def activate_license(license_key): """Activate the license for this machine""" response = requests.post( f'https://api.chariow.com/v1/licenses/{license_key}/activate', headers={'Authorization': 'Bearer sk_live_your_api_key'}, json={'device_identifier': get_machine_id()} ) return response.json() # On application startup license_key = load_stored_license() license = get_license(license_key) if license['status'] == 'pending_activation': # First run on this machine: claim a seat. # Products with requires_activation set to false never reach this branch. activate_license(license_key) elif not license['is_active'] or license['is_expired']: show_license_invalid_dialog() ``` *** ## 3. E-commerce Platform Plugin Create a plugin or integration for e-commerce platforms (WordPress, Shopify, etc.) to sell Chariow products. ### Scenario You want to extend an existing e-commerce platform to sell digital products managed in Chariow, synchronizing products and processing orders. ### Implementation Periodically fetch products from Chariow and sync them to your platform's database. Store Chariow product IDs alongside platform cart items. On checkout, create Chariow checkout sessions for each digital product. Listen to Chariow [Pulses](/en/guides/pulses) to mark orders as fulfilled in your platform. ### Code Example ```php theme={null} [ 'Authorization' => 'Bearer ' . get_option('chariow_api_key') ] ]); $products = json_decode(wp_remote_retrieve_body($response), true); foreach ($products['data'] as $product) { update_or_create_wc_product($product); } } // Process checkout for Chariow product function process_chariow_checkout($order_id) { $order = wc_get_order($order_id); foreach ($order->get_items() as $item) { $chariow_product_id = get_post_meta($item->get_product_id(), '_chariow_product_id', true); if ($chariow_product_id) { $response = wp_remote_post('https://api.chariow.com/v1/checkout', [ 'headers' => [ 'Authorization' => 'Bearer ' . get_option('chariow_api_key'), 'Content-Type' => 'application/json' ], 'body' => json_encode([ 'product_id' => $chariow_product_id, 'email' => $order->get_billing_email(), 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'phone' => [ 'number' => preg_replace('/[^0-9]/', '', $order->get_billing_phone()), 'country_code' => $order->get_billing_country() ] ]) ]); $result = json_decode(wp_remote_retrieve_body($response), true); update_post_meta($order_id, '_chariow_sale_id', $result['data']['purchase']['id']); } } } ``` *** ## 4. Course Platform with Access Control Build an online learning platform where course access is controlled by Chariow purchases. ### Scenario You run an educational platform and want to sell courses, controlling access based on purchase status and managing enrollments. ### Implementation Set up course-type products in Chariow with your curriculum content. When users try to access course content, verify their purchase using the [Get Sale](/api-reference/sales/get-sale) endpoint. Use [Pulses](/en/guides/pulses) to automatically enroll users when purchases complete. Store course progress in your database, linked to the Chariow customer ID. ### Code Example ```javascript theme={null} // Express.js middleware for course access control async function verifyCourseAccess(req, res, next) { const { courseSlug } = req.params; const userEmail = req.user.email; // Find the sale for this user and course const salesResponse = await fetch( `https://api.chariow.com/v1/sales?product_slug=${courseSlug}&customer_email=${userEmail}`, { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } } ); const sales = await salesResponse.json(); const validSale = sales.data.find(sale => sale.status === 'completed'); if (!validSale) { return res.status(403).json({ error: 'Access denied', message: 'Please purchase this course to access the content', purchase_url: `/checkout/${courseSlug}` }); } req.sale = validSale; next(); } // Pulse webhook handler for auto-enrollment app.post('/webhooks/chariow', async (req, res) => { const { event, data } = req.body; if (event === 'sale.completed') { const { customer, product } = data; // Auto-enroll user in course await db.enrollments.create({ user_email: customer.email, course_id: product.id, enrolled_at: new Date(), sale_id: data.id }); // Send welcome email await sendCourseWelcomeEmail(customer.email, product.name); } res.status(200).send('OK'); }); ``` *** ## 5. Affiliate and Campaign Tracking Track sales attribution across marketing campaigns and affiliate partners. ### Scenario You run marketing campaigns or have affiliate partners and need to track which sales come from which sources to calculate commissions or measure ROI. ### Implementation Generate unique campaign identifiers for each marketing channel or affiliate. Include the `campaign_id` parameter when initiating checkouts. Capture campaign data from sale Pulses to attribute conversions. Use the [List Sales](/api-reference/sales/list-sales) endpoint with campaign filters to generate attribution reports. ### Code Example ```javascript theme={null} // Track campaign from URL parameters function getCheckoutDataWithCampaign(productId, customerData) { const urlParams = new URLSearchParams(window.location.search); const campaignId = urlParams.get('ref') || urlParams.get('utm_campaign') || urlParams.get('affiliate'); const checkoutData = { product_id: productId, email: customerData.email, first_name: customerData.firstName, last_name: customerData.lastName, phone: { number: customerData.phone, country_code: customerData.countryCode } }; if (campaignId) { checkoutData.campaign_id = campaignId; } return checkoutData; } // Backend: Generate affiliate report async function generateAffiliateReport(affiliateId, startDate, endDate) { const response = await fetch( `https://api.chariow.com/v1/sales?campaign_id=${affiliateId}&from=${startDate}&to=${endDate}`, { headers: { 'Authorization': 'Bearer sk_live_your_api_key' } } ); const sales = await response.json(); const report = { affiliate_id: affiliateId, period: { start: startDate, end: endDate }, total_sales: sales.data.length, total_revenue: sales.data.reduce((sum, sale) => sum + sale.amount.value, 0), commission: 0 }; // Calculate 20% commission report.commission = report.total_revenue * 0.20; return report; } ``` *** ## Next Steps Learn checkout API best practices for production Deep dive into the checkout flow Set up webhooks for real-time notifications Explore the complete API documentation # Authentication Source: https://chariow.dev/en/introduction/authentication Learn how to authenticate your API requests to Chariow The Chariow API uses API keys to authenticate requests. ## Creating an API Key Go to [app.chariow.com](https://app.chariow.com) and log in to your account. Click on **Settings** in the sidebar. Select **API Keys** from the settings menu. Click **Create API Key**, give it a descriptive name, and copy the generated key. Copy your API key immediately after creation. For security reasons, the full key is only shown once. ## Making Authenticated Requests Include your API key in the `Authorization` header of every request: ```bash theme={null} Authorization: Bearer YOUR_API_KEY ``` ### Example Request ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/store" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/store', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Bearer YOUR_API_KEY' } response = requests.get('https://api.chariow.com/v1/store', headers=headers) ``` ```php PHP theme={null} $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.chariow.com/v1/store'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer YOUR_API_KEY' ]); ``` ## Key Security Best Practices Store API keys in environment variables, not in your codebase: ```bash theme={null} # .env file CHARIOW_API_KEY=your_api_key # Access in your code process.env.CHARIOW_API_KEY ``` Create separate API keys for development, staging, and production environments. This limits the impact if a key is compromised. Periodically create new keys and deprecate old ones. This limits the window of opportunity for compromised keys. Regularly review your API key usage in the dashboard. Look for unusual patterns that might indicate unauthorised access. Never expose your API key in client-side code (JavaScript running in browsers). All API calls should be made from your server. ## Authentication Errors If authentication fails, you'll receive a `401 Unauthorised` response: ```json theme={null} { "message": "API key is missing. Please provide a valid API key.", "data": [], "errors": [] } ``` Common causes of authentication failures: | Error | Cause | Solution | | -------------- | -------------------------------- | ------------------------------------ | | Missing header | No `Authorization` header | Add the header to your request | | Invalid key | Key doesn't exist or was deleted | Generate a new key in your dashboard | | Wrong store | Key belongs to a different store | Use the correct key for your store | ## Rate Limiting API keys are subject to rate limiting to ensure fair usage: * **100 requests per minute** per API key When rate limited, you'll receive a `429 Too Many Requests` response. Need higher rate limits? Contact our support team at [support@chariow.com](mailto:support@chariow.com). ## Next Steps Explore the complete API documentation Learn more about rate limiting # Introduction Source: https://chariow.dev/en/introduction/overview Welcome to Chariow - The all-in-one platform for selling digital products ## What is Chariow? Chariow is a powerful e-commerce platform designed specifically for creators and businesses selling digital products. Whether you're selling courses, software licenses, downloadable files, or digital services, Chariow provides everything you need to manage your store, process payments, and deliver products to your customers. Get up and running with the Chariow API in minutes Learn how to authenticate your API requests Explore the complete API documentation Connect Chariow to AI assistants via Model Context Protocol ## Base URL All API requests should be made to: ``` https://api.chariow.com/v1 ``` ## Response Format All API responses follow a consistent JSON structure: ```json theme={null} { "message": "success", "data": { // Response data }, "errors": [] } ``` ### Successful Response ```json theme={null} { "message": "success", "data": { "id": "prd_abc123", "name": "Premium Course", "price": { "value": 99, "formatted": "$99.00", "short": "99", "currency": "USD" } }, "errors": [] } ``` ### Error Response ```json theme={null} { "message": "Validation failed", "data": [], "errors": { "email": ["The email field is required."], "product_id": ["The selected product is invalid."] } } ``` ### Pagination List endpoints use cursor-based pagination: ```json theme={null} { "message": "success", "data": { "data": [...], "pagination": { "next_cursor": "eyJpZCI6NTB9", "prev_cursor": null, "has_more": true } }, "errors": [] } ``` Use the `cursor` query parameter to navigate pages: ``` GET /v1/products?cursor=eyJpZCI6NTB9&per_page=20 ``` ## Rate Limits The API implements rate limiting to ensure fair usage: | Endpoint Type | Limit | | ---------------- | ------------------- | | All API requests | 100 requests/minute | Rate limits are applied per API key. Each response includes rate limit headers: ```http theme={null} X-RateLimit-Limit: 100 X-RateLimit-Remaining: 98 X-RateLimit-Reset: 1642089600 ``` When you exceed the rate limit, you'll receive a `429 Too Many Requests` response: ```json theme={null} { "message": "Too many requests. Please try again later.", "data": [], "errors": [] } ``` Need higher rate limits? Contact us at [support@chariow.com](mailto:support@chariow.com) to discuss enterprise options. ## Key Features Sell any type of digital product: * **Downloadable files** - PDFs, videos, music, software * **Online courses** - With chapters, lessons, and progress tracking * **Software licenses** - Automatic license key generation and activation * **Bundles** - Combine multiple products into one offering * **Services** - Book consultations or digital services Multiple pricing options to suit your business: * One-time payments * Pay-what-you-want pricing * Free products for lead generation * Discount codes and promotions Complete customer lifecycle management: * Customer profiles and purchase history * License key management * Automated email notifications * Customer portal access Build custom integrations with our comprehensive API: * RESTful API with JSON responses * Pulse notifications for real-time events * MCP integration for AI assistants * SDK support (coming soon) ## Use Cases Build and sell online courses with video hosting, progress tracking, and completion certificates. Distribute software with automatic license key generation, activation limits, and expiration management. Sell templates, design assets, and digital resources with instant delivery. Monetise ebooks, music, photography, and other creative works. ## Getting Help Browse our knowledge base Join our community Contact our support team # Quick Start Source: https://chariow.dev/en/introduction/quickstart Get started with the Chariow API in minutes This guide will help you make your first API request to Chariow in under 5 minutes. ## Prerequisites Before you begin, you'll need: A Chariow account with an active store An API key from your store settings ## Step 1: Get Your API Key Go to [app.chariow.com](https://app.chariow.com) and sign in to your account. Click on **Settings** β†’ **API Keys** in your store dashboard. Click **Create API Key**, give it a name (e.g., "Development"), and copy the generated key. Your API key will only be shown once. Make sure to copy and store it securely. ## Step 2: Make Your First Request Let's verify your API key by fetching your store information: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/store" \ -H "Authorization: Bearer sk_live_your_api_key_here" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/store', { headers: { 'Authorization': 'Bearer sk_live_your_api_key_here' } }); const data = await response.json(); console.log(data); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.chariow.com/v1/store', headers={'Authorization': 'Bearer sk_live_your_api_key_here'} ) print(response.json()) ``` ```php PHP theme={null} $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.chariow.com/v1/store'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_your_api_key_here' ]); $response = curl_exec($ch); curl_close($ch); print_r(json_decode($response, true)); ``` You should receive a response like this: ```json Response theme={null} { "message": "success", "data": { "id": "str_abc123xyz", "name": "My Awesome Store", "description": "Selling digital products", "logo_url": "https://cdn.chariow.com/stores/logo.png", "url": "https://mystore.chariow.com", "status": "active" }, "errors": [] } ``` ## Step 3: List Your Products Now let's fetch your published products: ```bash cURL theme={null} curl -X GET "https://api.chariow.com/v1/products" \ -H "Authorization: Bearer sk_live_your_api_key_here" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/products', { headers: { 'Authorization': 'Bearer sk_live_your_api_key_here' } }); const data = await response.json(); console.log(data); ``` ## Step 4: Initiate a Checkout Create a checkout session to process a sale: ```bash cURL theme={null} curl -X POST "https://api.chariow.com/v1/checkout" \ -H "Authorization: Bearer sk_live_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{ "product_id": "prd_abc123", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": { "number": "1234567890", "country_code": "US" } }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.chariow.com/v1/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: 'prd_abc123', email: 'customer@example.com', first_name: 'John', last_name: 'Doe', phone: { number: '1234567890', country_code: 'US' } }) }); const data = await response.json(); console.log(data); ``` ## What's Next? Learn more about API authentication Learn how to work with products Master the checkout flow Set up real-time notifications # MCP Overview Source: https://chariow.dev/en/mcp/overview Connect your Chariow store to AI assistants like Claude and ChatGPT The Chariow MCP server lets AI tools access live data from your store through the [Model Context Protocol](https://modelcontextprotocol.io) standard. Once connected, you can query sales, customers, products, and analytics using natural language. ## Why use Chariow MCP? One-click OAuth authentication. No API keys to manage. Read products, customers, sales, licenses, discounts, and analytics. Data formatted for AI assistants to understand and use effectively. ## What can you do? | Use case | Example prompt | | ----------------------- | ---------------------------------------------------------------------------- | | **Check sales** | "Show me today's completed sales" | | **Find customers** | "Find the customer with email [marie@example.com](mailto:marie@example.com)" | | **Analyse performance** | "What's my conversion rate this month?" | | **Manage licenses** | "How many activations are left on license ABC-123?" | | **Search everything** | "Search for 'premium' across my store" | ## How it works Chariow MCP connection flow 1. You connect your AI tool to `https://mcp.chariow.com/public` 2. OAuth authenticates you with your Chariow account 3. The AI can now read your store data through 21 available tools ## Supported AI tools | Tool | Connection method | Setup guide | | ------------------- | ----------------- | ------------------------------------ | | Claude Desktop | Custom connector | [View guide](/en/mcp/setup#claude) | | Claude.ai (Pro/Max) | Custom connector | [View guide](/en/mcp/setup#claude) | | ChatGPT | Connector | [View guide](/en/mcp/setup#chatgpt) | | Cursor | Remote MCP server | [View guide](/en/mcp/setup#cursor) | | Windsurf | Remote MCP server | [View guide](/en/mcp/setup#windsurf) | ## MCP endpoint Connect your AI tools to: ``` https://mcp.chariow.com/public ``` This endpoint supports both Streamable HTTP and SSE (Server-Sent Events). ## Get started Connect your AI tool in minutes See all 21 available tools # Security best practices Source: https://chariow.dev/en/mcp/security Understand data sharing and security when using Chariow MCP When you connect an AI tool to your Chariow store via MCP, you're granting that tool access to your store data. This page explains what data is shared and how to stay secure. ## What data is accessible? Once connected, the AI tool can read: | Data type | What's included | | ------------- | -------------------------------------------------------------- | | **Store** | Name, description, logo, URL, settings, subscription status | | **Products** | Names, descriptions, pricing, images, categories, sales counts | | **Customers** | Names, emails, phone numbers, purchase history | | **Sales** | Amounts, payment details, shipping addresses, customer info | | **Discounts** | Codes, values, usage counts, restrictions | | **Licenses** | Keys, activation counts, status, expiry dates | | **Analytics** | Revenue, visits, conversion rates, traffic sources | **All MCP tools are read-only.** The AI cannot modify your store, create products, or process transactions. However, it can read all data associated with your store. ## Who can see your data? When you connect via MCP: 1. **Your AI tool** (Claude, ChatGPT, etc.) receives your store data to answer your questions 2. **The AI provider** may process and store this data according to their privacy policy 3. **Chariow** acts as the bridge and does not store conversation data **Review your AI provider's privacy policy.** Each provider handles data differently. Chariow is not responsible for how third-party AI providers process, store, or use your data. ## Security recommendations ### Only connect trusted tools Only use MCP connections from AI providers you trust. Verify you're connecting to official tools: * Claude from Anthropic * ChatGPT from OpenAI * Cursor, Windsurf, or other reputable tools ### Verify the MCP endpoint Always confirm you're connecting to the official Chariow endpoint: * `https://mcp.chariow.com/public` ### Be mindful of prompts AI assistants follow instructions, including those hidden in data. Avoid: * Pasting untrusted content into conversations * Asking the AI to process external URLs or files without review ### Review access regularly Periodically check your connected applications: 1. Go to your [Chariow Dashboard](https://app.chariow.com) 2. Navigate to **Settings** β†’ **API Keys** 3. Revoke any connections you no longer use ### Use separate stores for testing If you're experimenting with MCP integrations, consider using a test store rather than your production store with real customer data. ## What Chariow does * **OAuth authentication**: Secure authorisation flow with no API keys stored in config files * **Read-only access**: All tools can only read data, never write * **Rate limiting**: 60 requests per minute prevents abuse * **HTTPS only**: All connections are encrypted ## What Chariow does not do * Store your conversations with AI tools * Share your data with other users or third parties * Control how AI providers process your data * Monitor what questions you ask ## Revoking access To disconnect an AI tool immediately: 1. Go to **Settings** β†’ **API Keys** in your Chariow Dashboard 2. Find the MCP connection 3. Click **Revoke** The AI tool will lose access to your store data immediately. *** ## Next steps Connect your AI tool See available tools # Get started with MCP Source: https://chariow.dev/en/mcp/setup Connect your Chariow store to Claude, ChatGPT, Cursor, and other AI tools This guide explains how to connect your AI tool to your Chariow store using the Model Context Protocol. ## MCP endpoint All AI tools connect to the same endpoint: ``` https://mcp.chariow.com/public ``` This endpoint supports both Streamable HTTP and SSE (Server-Sent Events). *** ## Claude Claude supports MCP through custom connectors on both Claude Desktop and Claude.ai (Pro/Max plans). In Claude Desktop or Claude.ai, go to **Settings** β†’ **Connectors**. Click **Add custom connector** and enter: * **URL:** `https://mcp.chariow.com/public` Click **Add**. You'll be redirected to Chariow to authorise access to your store. Open a new conversation and ask Claude about your store. For example: "Show me my recent sales". Custom connectors require Claude Pro, Max, Team, or Enterprise. See [Claude's MCP documentation](https://support.claude.com/en/articles/11175166-getting-started-with-custom-connectors-using-remote-mcp) for details. *** ## ChatGPT ChatGPT supports MCP connectors for accessing external tools. Go to **Settings** β†’ **Apps & Connectors** β†’ **Advanced settings** and enable developer mode. Navigate to **Settings** β†’ **Connectors** β†’ **Create** and enter: * **Connector name:** Chariow * **Description:** Access your Chariow store data * **Connector URL:** `https://mcp.chariow.com/public` Complete the OAuth flow to connect your Chariow account. Start a new chat, click **+**, select **More**, and choose Chariow from your available tools. ChatGPT will show tool-call confirmations. Write operations require approval unless you choose to remember your decision. *** ## Cursor Cursor connects to remote MCP servers through configuration files. Go to **Settings** β†’ **Features** β†’ **MCP**. Click **Add new MCP server** and select **SSE** as the type. Or create a configuration file at `.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "chariow": { "url": "https://mcp.chariow.com/public" } } } ``` When you first use a Chariow tool, you'll be prompted to authenticate via OAuth. See [Cursor's MCP documentation](https://cursor.com/docs/context/mcp) for more configuration options. *** ## Windsurf Windsurf supports remote MCP servers through its extensions settings. Go to **Settings** β†’ **Extensions** β†’ **MCP**. Add a new MCP server with: ```json theme={null} { "mcpServers": { "chariow": { "url": "https://mcp.chariow.com/public" } } } ``` Complete the OAuth flow when prompted to connect your Chariow account. *** ## Claude Code (CLI) For Claude Code, add the Chariow server using the CLI: ```bash theme={null} claude mcp add --transport http chariow https://mcp.chariow.com/public ``` You'll be prompted to authenticate via OAuth on first use. *** ## Verify connection After setup, verify your connection is working: 1. Start a new conversation with your AI tool 2. Ask: **"Show me my Chariow store information"** 3. The AI should display your store name, URL, and settings Try these example prompts: * "List my recent sales" * "How many customers do I have?" * "What are my best-selling products?" *** ## Troubleshooting * Verify the URL is correct: `https://mcp.chariow.com/public` * Restart your AI tool completely * Try removing and re-adding the connector * Ensure you're logged into your Chariow account * Check that your store is active * Clear browser cookies and try again * Restart your AI tool * For file-based config, verify JSON syntax is valid * In ChatGPT, click **Refresh** in Connectors settings The MCP server allows 60 requests per minute. Wait a moment before continuing. *** ## Revoke access To disconnect an AI tool from your store: 1. Go to your [Chariow Dashboard](https://app.chariow.com) 2. Navigate to **Settings** β†’ **API Keys** 3. Find the MCP connection and click **Revoke** *** ## Next steps See all 21 available tools Learn about data sharing and security # Supported tools Source: https://chariow.dev/en/mcp/tools All available MCP tools for querying your Chariow store The Chariow MCP server provides 21 read-only tools for accessing your store data. All tools are idempotent and cannot modify your store. ## Tools reference | Tool | Description | Sample prompt | | ------------------------------- | -------------------------------------------------------------------- | ----------------------------------------- | | `global_search` | Search across all store data (products, customers, sales, discounts) | "Search for 'premium' in my store" | | `get_store` | Get store profile, settings, and sales summary | "Show me my store information" | | `list_products` | List products with optional filters (status, category, type) | "List my published products" | | `get_product` | Get full details for a specific product | "Show me details for product prd\_abc123" | | `list_customers` | List customers with optional search | "Find customers named Marie" | | `get_customer` | Get full profile for a specific customer | "Show me customer cus\_abc123" | | `list_sales` | List sales with filters (status, date range, customer) | "Show me today's completed sales" | | `get_sale` | Get complete details for a specific sale | "Get details for sale sal\_xyz789" | | `list_discounts` | List discount codes with optional filters | "List my active discount codes" | | `get_discount` | Get full details for a specific discount | "How many times has SUMMER20 been used?" | | `list_licenses` | List issued licenses with filters (status, customer, product) | "List all active licenses" | | `get_license` | Get details for a specific license key | "Check license ABC-123-XYZ-789" | | `get_license_activations` | View activation history for a license | "Show activations for license ABC-123" | | `list_pulses` | List webhook configurations | "List my webhooks" | | `get_pulse` | Get details for a specific webhook | "Show me pulse pulse\_abc123" | | `get_store_analytics` | Get store performance overview (visits, conversions, sales) | "Show me store analytics for this month" | | `get_sales_analytics` | Get detailed revenue and sales analysis | "What's my revenue this month?" | | `get_customer_analytics` | Get customer insights (new vs returning, geography) | "Who are my top 5 customers?" | | `get_visits_analytics` | Get traffic analysis (sources, devices, locations) | "Where is my traffic coming from?" | | `get_conversion_rate_analytics` | Get conversion rates by device, country, product | "What's my conversion rate?" | *** ## Tool details ### global\_search Search across all your store data at once. | Parameter | Type | Required | Description | | --------- | ------- | -------- | --------------------------------------- | | `query` | string | Yes | Search term | | `limit` | integer | No | Results per category (1-10). Default: 5 | | `from` | string | No | Start date (YYYY-MM-DD) | | `to` | string | No | End date (YYYY-MM-DD) | *** ### get\_store Get your store profile, settings, and sales summary. No parameters required. *** ### list\_products List products with filtering and pagination. | Parameter | Type | Required | Description | | ---------- | ------- | -------- | ---------------------------------------------------------------------------- | | `search` | string | No | Search by name or slug | | `status` | string | No | Filter: `draft`, `published`, `archived` | | `category` | string | No | Filter by category | | `type` | string | No | Filter: `downloadable`, `service`, `course`, `license`, `bundle`, `coaching` | | `per_page` | integer | No | Results per page (1-100). Default: 20 | | `cursor` | string | No | Pagination cursor | *** ### get\_product Get full details for a specific product. | Parameter | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------ | | `product_id` | string | Yes | Product ID (`prd_xxx`) or slug | *** ### list\_customers List customers with optional search. | Parameter | Type | Required | Description | | ---------- | ------- | -------- | ------------------------------------- | | `search` | string | No | Search by name or email | | `per_page` | integer | No | Results per page (1-100). Default: 20 | | `cursor` | string | No | Pagination cursor | *** ### get\_customer Get full profile for a specific customer. | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ----------------------- | | `customer_id` | string | Yes | Customer ID (`cus_xxx`) | *** ### list\_sales List sales with filtering options. | Parameter | Type | Required | Description | | ------------- | ------- | -------- | ----------------------------------------------------- | | `status` | string | No | Filter: `completed`, `failed`, `pending`, `abandoned` | | `customer_id` | string | No | Filter by customer | | `start_date` | string | No | From date (YYYY-MM-DD) | | `end_date` | string | No | To date (YYYY-MM-DD) | | `per_page` | integer | No | Results per page (1-100). Default: 10 | | `cursor` | string | No | Pagination cursor | *** ### get\_sale Get complete details for a specific sale. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------- | | `sale_id` | string | Yes | Sale ID (`sal_xxx`) | *** ### list\_discounts List discount codes with optional filters. | Parameter | Type | Required | Description | | ---------- | ------- | -------- | --------------------------------------- | | `status` | string | No | Filter: `active`, `expired`, `disabled` | | `search` | string | No | Search by code or name | | `per_page` | integer | No | Results per page (1-100). Default: 20 | | `cursor` | string | No | Pagination cursor | *** ### get\_discount Get full details for a specific discount. | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ----------------------- | | `discount_id` | string | Yes | Discount ID (`dis_xxx`) | *** ### list\_licenses List issued licenses with filters. | Parameter | Type | Required | Description | | ------------- | ------- | -------- | -------------------------------------- | | `status` | string | No | Filter: `active`, `expired`, `revoked` | | `customer_id` | string | No | Filter by customer | | `product_id` | string | No | Filter by product | | `per_page` | integer | No | Results per page (1-100). Default: 20 | | `cursor` | string | No | Pagination cursor | *** ### get\_license Get details for a specific license key. | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------- | | `license_key` | string | Yes | License key (e.g., `ABC-123-XYZ-789`) | *** ### get\_license\_activations View activation history for a license. | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ----------- | | `license_key` | string | Yes | License key | *** ### list\_pulses List webhook configurations. | Parameter | Type | Required | Description | | ---------- | ------- | -------- | ------------------------------------- | | `search` | string | No | Search webhooks | | `per_page` | integer | No | Results per page (1-100). Default: 20 | | `cursor` | string | No | Pagination cursor | *** ### get\_pulse Get details for a specific webhook. | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ---------------------- | | `pulse_id` | string | Yes | Pulse ID (`pulse_xxx`) | *** ### get\_store\_analytics Get store performance overview. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------- | | `from` | string | Yes | Start date (YYYY-MM-DD) | | `to` | string | Yes | End date (YYYY-MM-DD) | *** ### get\_sales\_analytics Get detailed revenue and sales analysis. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------- | | `from` | string | Yes | Start date (YYYY-MM-DD) | | `to` | string | Yes | End date (YYYY-MM-DD) | *** ### get\_customer\_analytics Get customer insights and top customers. | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------ | | `from` | string | Yes | Start date (YYYY-MM-DD) | | `to` | string | Yes | End date (YYYY-MM-DD) | | `top_customers_limit` | integer | No | Number of top customers. Default: 10 | *** ### get\_visits\_analytics Get traffic analysis. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------- | | `from` | string | Yes | Start date (YYYY-MM-DD) | | `to` | string | Yes | End date (YYYY-MM-DD) | *** ### get\_conversion\_rate\_analytics Get conversion rates by device, country, and product. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------- | | `from` | string | Yes | Start date (YYYY-MM-DD) | | `to` | string | Yes | End date (YYYY-MM-DD) | *** ## Rate limits The MCP server allows an average of **60 requests per minute** per connection. If you hit rate limits, wait a moment before continuing. *** ## Next steps Connect your AI tool Learn about data sharing # n8n Integration Source: https://chariow.dev/en/n8n/overview Automate your Chariow store workflows with the official n8n community node The Chariow n8n integration lets you connect your store to hundreds of other apps and automate workflows β€” from syncing sales to a CRM, to sending licence keys via email, to triggering Slack notifications on new orders. The Chariow n8n node is currently available for **self-hosted n8n instances only**. It is not yet listed on the n8n community nodes registry. See the [setup guide](/en/n8n/setup) for installation instructions. ## Why use Chariow with n8n? Build multi-step automations triggered by store events β€” no code required. Access products, sales, customers, licences, discounts, affiliates, and more. React to sales, licence changes, and affiliate events in real time. *** ## Available nodes The integration provides two n8n nodes: | Node | Type | Description | | ------------------- | ------- | --------------------------------------------------------------------------------- | | **Chariow** | Action | Perform operations on your store resources (get, list, create, activate, revoke). | | **Chariow Trigger** | Trigger | Start workflows automatically when events occur in your store. | *** ## Chariow node resources The **Chariow** action node supports the following resources and operations: | Resource | Operations | | ------------- | ------------------------------- | | **Affiliate** | Get, Get Many | | **Checkout** | Create | | **Customer** | Get, Get Many | | **Discount** | Get, Get Many | | **Licence** | Get, Get Many, Activate, Revoke | | **Product** | Get, Get Many | | **Pulse** | Get, Get Many | | **Sale** | Get, Get Many | | **Store** | Get | *** ## Trigger events The **Chariow Trigger** node listens for these webhook events: | Event | Description | | --------------------- | --------------------------------------------------- | | **Sale Completed** | Fires when a sale is successfully completed. | | **Sale Refunded** | Fires when a sale is refunded. | | **Sale Disputed** | Fires when a chargeback or dispute is opened. | | **Licence Created** | Fires when a new licence key is generated. | | **Licence Activated** | Fires when a licence is activated on a device. | | **Licence Revoked** | Fires when a licence is revoked. | | **Licence Expired** | Fires when a licence reaches its expiry date. | | **Affiliate Sale** | Fires when an affiliate-referred sale is completed. | *** ## AI tool support Both the **Chariow** and **Chariow Trigger** nodes are compatible with n8n's AI Agent functionality. You can use them as tools within an AI Agent node to let your AI workflows query store data or react to store events. *** ## Get started Install the node on your self-hosted n8n instance Generate an API key for your store # n8n Setup Source: https://chariow.dev/en/n8n/setup Install and configure the Chariow node on your self-hosted n8n instance This guide walks you through installing the Chariow community node on a self-hosted n8n instance and connecting it to your store. ## Prerequisites Before you begin, make sure you have: * A **self-hosted n8n instance** (v1.0 or later) * A **Chariow API key** (starts with `sk_`) β€” generate one from **Settings** β†’ **API Keys** in your [Chariow Dashboard](https://app.chariow.com) * **Node.js >= 18.10** on the machine running n8n *** ## Installation The Chariow node is not yet available through the n8n Community Nodes UI. You must install it manually using one of the methods below. ### npm (recommended) Run the following command in your n8n user directory: ```bash theme={null} cd ~/.n8n && npm install @chariow/n8n-nodes-chariow ``` Then restart n8n for the node to appear. ### Docker If you run n8n in Docker, set the `N8N_CUSTOM_EXTENSIONS` environment variable to install the package on startup: ```yaml theme={null} # docker-compose.yml services: n8n: image: n8nio/n8n environment: - N8N_CUSTOM_EXTENSIONS=@chariow/n8n-nodes-chariow # ... your other config ``` Restart the container after updating the configuration. Community Nodes UI support is planned. Once available, you will be able to install directly from **Settings** β†’ **Community Nodes** inside n8n. *** ## Configure credentials In n8n, go to **Settings** β†’ **Credentials** β†’ **Add Credential**. Search for **Chariow API** and select it. Paste your Chariow API key (starts with `sk_`) into the **API Key** field. Click **Save**. n8n will test the connection automatically. A green tick confirms your key is valid. *** ## Verify installation Click **New Workflow** in n8n. Click **+** to add a node and search for **Chariow**. You should see two nodes: * **Chariow** β€” for performing actions (get, list, create) * **Chariow Trigger** β€” for starting workflows from store events *** ## Troubleshooting * Make sure you installed the package in the correct directory (`~/.n8n` for npm installs) * Restart n8n completely after installation * Check the n8n logs for any installation errors: `docker logs n8n` or check your process output * Verify the package is installed: `ls ~/.n8n/node_modules/@chariow` * Confirm your API key starts with `sk_` and is copied in full * Check that your store is active in the Chariow dashboard * Ensure your n8n instance can reach `https://api.chariow.com` (no firewall or proxy blocking) * Verify the `N8N_CUSTOM_EXTENSIONS` environment variable is set correctly * Rebuild and restart the container: `docker compose down && docker compose up -d` * Check container logs for npm install errors: `docker logs ` *** ## Next steps See all resources and operations Browse the 8 available trigger events # Error Handling Source: https://chariow.dev/en/resources/errors Understanding and handling API errors The Chariow API uses conventional HTTP response codes to indicate the success or failure of requests. ## Response Format All API responses follow a consistent format: ```json theme={null} { "message": "Success message or error description", "data": {}, "errors": [] } ``` ## HTTP Status Codes | Code | Description | | ----- | -------------------------------------------- | | `200` | Success - Request completed successfully | | `201` | Created - Resource created successfully | | `400` | Bad Request - Invalid request parameters | | `401` | Unauthorised - Invalid or missing API key | | `403` | Forbidden - Access denied to this resource | | `404` | Not Found - Resource doesn't exist | | `422` | Unprocessable Entity - Validation failed | | `429` | Too Many Requests - Rate limit exceeded | | `500` | Internal Server Error - Something went wrong | ## Common Errors ### Authentication Errors (401) ```json theme={null} { "message": "Store API key is required", "data": [], "errors": [] } ``` **Causes:** * Missing `Authorization` header * Invalid API key format * Revoked or expired API key **Solution:** ```bash theme={null} # Ensure you include the Authorization header curl -X GET "https://api.chariow.com/v1/store" \ -H "Authorization: Bearer sk_live_your_api_key" ``` ### Resource Not Found (404) ```json theme={null} { "message": "No query results for model [App\\Models\\Product].", "data": [], "errors": [] } ``` **Causes:** * Invalid resource ID * Resource belongs to a different store * Resource has been deleted * Product is not published (for public endpoints) ### Validation Errors (422) ```json theme={null} { "message": "The given data was invalid.", "data": [], "errors": { "email": ["The email field is required."], "product_id": ["The selected product_id is invalid."] } } ``` **Causes:** * Missing required fields * Invalid field format * Business rule violations ### Rate Limit Exceeded (429) ```json theme={null} { "message": "Rate limit exceeded. Please retry after 60 seconds.", "data": [], "errors": [] } ``` **Solution:** Wait for the specified time and retry. See [Rate Limits](/en/resources/rate-limits) for details. ## Handling Errors ### JavaScript Example ```javascript theme={null} async function makeRequest(endpoint, options = {}) { const response = await fetch(`https://api.chariow.com/v1${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', ...options.headers } }); const data = await response.json(); if (!response.ok) { switch (response.status) { case 401: throw new AuthenticationError(data.message); case 404: throw new NotFoundError(data.message); case 422: throw new ValidationError(data.message, data.errors); case 429: throw new RateLimitError(data.message); default: throw new ApiError(data.message, response.status); } } return data; } // Usage try { const product = await makeRequest('/products/prd_abc123'); console.log(product); } catch (error) { if (error instanceof NotFoundError) { console.log('Product not found'); } else if (error instanceof ValidationError) { console.log('Validation failed:', error.errors); } else { console.error('API error:', error.message); } } ``` ### PHP Example ```php theme={null} = 400) { throw new Exception($result['message'], $httpCode); } return $result; } // Usage try { $product = makeRequest('/products/prd_abc123'); print_r($product); } catch (Exception $e) { if ($e->getCode() === 404) { echo "Product not found\n"; } else { echo "API error: " . $e->getMessage() . "\n"; } } ``` ## Error Codes Reference ### Checkout Errors | Message | Cause | Solution | | ----------------------- | --------------------------------- | --------------------------------- | | `Product not found` | Invalid product ID or unpublished | Use a valid, published product ID | | `Already purchased` | Customer owns this product | Redirect to their purchase | | `Product unavailable` | Out of stock or quantity limit | Check product availability | | `Invalid discount code` | Code doesn't exist or expired | Verify the discount code | ### License Errors | Message | Cause | Solution | | --------------------------- | ------------------------ | --------------------------- | | `License not found` | Invalid license key | Verify the license key | | `License already activated` | Reached activation limit | Show user their activations | | `License expired` | Past expiration date | Prompt for renewal | | `License revoked` | Manually revoked | Contact support | ## Best Practices Don't assume success - always check the HTTP status code before processing the response. Log error responses for debugging and monitoring purposes. Translate API errors into user-friendly messages for your customers. For 5xx errors and rate limits, implement automatic retry with exponential backoff. # Rate Limits Source: https://chariow.dev/en/resources/rate-limits Understand API rate limiting and how to handle it The Chariow API implements rate limiting to ensure fair usage and maintain service quality for all users. ## Default Limits | Endpoint Type | Limit | Window | | ---------------- | ------------ | ---------- | | All API requests | 100 requests | per minute | Rate limits are applied per API key. Each API key has its own separate limit counter. ## Rate Limit Headers Every API response includes headers indicating your current rate limit status: ```http theme={null} X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 950 X-RateLimit-Reset: 1642089600 ``` | Header | Description | | ----------------------- | -------------------------------------- | | `X-RateLimit-Limit` | Maximum requests allowed in the window | | `X-RateLimit-Remaining` | Requests remaining in current window | | `X-RateLimit-Reset` | Unix timestamp when the limit resets | ## Rate Limit Exceeded When you exceed the rate limit, you'll receive a `429 Too Many Requests` response: ```json theme={null} { "message": "Rate limit exceeded. Please retry after 60 seconds.", "data": [], "errors": [] } ``` The response includes a `Retry-After` header indicating how long to wait: ```http theme={null} HTTP/1.1 429 Too Many Requests Retry-After: 60 ``` ## Handling Rate Limits ### Exponential Backoff Implement exponential backoff to gracefully handle rate limits: ```javascript theme={null} async function fetchWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options); if (response.status === 429) { const retryAfter = response.headers.get('Retry-After') || 60; const delay = Math.min(retryAfter * 1000, Math.pow(2, attempt) * 1000); console.log(`Rate limited. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); continue; } return response; } throw new Error('Max retries exceeded'); } ``` ### Request Batching Batch multiple operations to reduce request count: ```javascript theme={null} // Instead of making 100 individual requests for (const customerId of customerIds) { await getCustomer(customerId); // 100 requests } // Use pagination to get multiple at once const customers = await listCustomers({ per_page: 100 }); // 1 request ``` ### Caching Cache responses to avoid unnecessary requests: ```javascript theme={null} const cache = new Map(); const CACHE_TTL = 60000; // 1 minute async function getCachedCustomer(customerId) { const cached = cache.get(customerId); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data; } const response = await fetch(`/v1/customers/${customerId}`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); const data = await response.json(); cache.set(customerId, { data, timestamp: Date.now() }); return data; } ``` ## Increasing Limits If you need higher rate limits: 1. **Enterprise Plans** - Higher limits are available on enterprise plans 2. **Contact Support** - Request a temporary increase for specific use cases 3. **Optimize Usage** - Review your implementation for optimization opportunities Contact [support@chariow.com](mailto:support@chariow.com) to discuss higher rate limits for your use case. ## Best Practices Track rate limit headers to understand your usage patterns and adjust accordingly. Instead of polling for changes, use webhooks to receive real-time notifications. Queue requests and process them at a controlled rate to avoid bursts. Cache data that doesn't change frequently to reduce API calls.