# Hoko API Documentation Full markdown corpus for Hoko's API documentation. ## Full API Documentation ### Introduction Hoko is a modern link attribution platform that empowers you to create branded short links, track conversions end-to-end, and measure marketing performance with precision. Canonical URL: https://hoko.to/docs/introduction Markdown URL: https://hoko.to/docs/introduction/README.md ## What is Hoko? Hoko is a comprehensive link attribution and conversion tracking platform designed for modern marketing teams. Whether you're running multi-channel campaigns, tracking affiliate performance, or measuring ROI, Hoko provides the tools you need to understand exactly how your marketing efforts drive results. Built with developers in mind, Hoko offers a powerful REST API that integrates seamlessly with your existing stack, enabling automated link management, real-time analytics, and sophisticated conversion tracking. ## Key Features Hoko combines powerful link management with advanced analytics and conversion tracking. Here's what makes it special: - Branded Short Links - Create memorable short URLs with your own domain for enhanced trust and brand recognition - Real-Time Analytics - Track clicks, conversions, and user behavior with detailed insights updated in real-time - Conversion Tracking - Connect clicks to leads and sales with flexible event tracking and customer attribution - REST API - Full programmatic access to all features with comprehensive endpoints for automation and integrations - Multi-Channel Attribution - Track performance across campaigns, partners, and channels with granular UTM parameter support - Partner Management - Organize and track affiliate partners with commission and performance analytics - Collections & Tags - Organize links efficiently with collections and tags for better campaign management - Customer Insights - Build comprehensive customer profiles by tracking their journey from click to conversion ## Use Cases Hoko is perfect for a wide range of marketing and attribution scenarios: - Marketing Campaigns - Track performance of email, social media, and paid advertising campaigns - Affiliate Programs - Manage partner links and track commission-based conversions - E-commerce Attribution - Connect marketing touchpoints to actual sales and revenue - Lead Generation - Track which channels and campaigns generate the highest-quality leads - Content Marketing - Measure the impact of blog posts, newsletters, and content shares - Multi-Tenant SaaS - Track links and conversions across different customer segments or tenants - API Integrations - Build custom integrations with CRM, marketing automation, and analytics tools ## Getting Started Ready to get started? Here's a quick overview of what you'll need: - Create a Hoko account and set up your workspace - Create your first collection to organize links - Generate an API key with appropriate scopes for your use case - Start creating links via the dashboard or API - Track conversions by integrating the conversion tracking endpoints > **Tip: Pro Tip** > Start with the Links section to understand how to create and manage short links, then explore Analytics to see how to track performance, and finally dive into Conversion Tracking to connect clicks to business outcomes. --- ### API Keys Create and manage API keys with granular permission scopes. Secure your integrations with the principle of least privilege. Canonical URL: https://hoko.to/docs/api-keys Markdown URL: https://hoko.to/docs/api-keys/README.md ## Overview API keys are the primary method of authenticating requests to the Hoko API. Each key is associated with your workspace and can be configured with specific permission scopes, allowing you to control exactly what operations each key can perform. This granular access control enables you to create different keys for different purposes—for example, a read-only key for analytics dashboards, or a write key for automated link creation scripts. ## Creating API Keys Create new API keys from your workspace dashboard under Settings > Integrations > API Keys. When creating a key, you'll need to: 1. Provide a descriptive name to help you identify the key's purpose 2. Select the appropriate permission scopes based on what operations the key needs to perform 3. Copy the key immediately after creation—it will only be displayed once for security reasons > **Warning: Important Security Notice** > API keys are displayed only once during creation. If you lose a key, you must revoke it and create a new one. Never share API keys publicly or commit them to version control. ## Permission Scopes Scopes define what operations an API key can perform. By granting only the minimum required permissions, you follow security best practices and limit potential damage if a key is compromised. Each scope is independent—you can combine multiple scopes to create keys with exactly the permissions you need. - linksRead - Read and list links (GET /api/links) - linksWrite - Create, update, and delete links (POST, PUT, DELETE /api/links) - collectionsRead - Read and list collections (GET /api/collections) - collectionsWrite - Create, update, and delete collections (POST, PUT, DELETE /api/collections) - tagsRead - Read and list tags (GET /api/tags) - tagsWrite - Create, update, and delete tags (POST, PUT, DELETE /api/tags) - analyticsRead - Access click analytics and performance data (GET /api/analytics) - conversionsWrite - Track lead and sale conversion events (POST /api/track/lead, POST /api/track/sale) - customersRead - Read customer data (GET /api/customers) - partnersRead - Read partner data (GET /api/partners) - partnersWrite - Create, update, and delete partners (POST, PUT, DELETE /api/partners) > **Tip: Best Practice** > Create separate API keys for different environments (development, staging, production) and different use cases (read-only monitoring, automated link creation, conversion tracking). This makes it easier to rotate keys and audit access. ## Using API Keys Once you have an API key, include it in every API request using the Authorization header with the Bearer scheme. The API will validate the key and check that it has the required scopes for the requested operation. **Header Format** ```text Authorization: Bearer ``` **curl Example** ```bash curl -X GET "https://hoko.to/api/links" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" ``` ## Revoking API Keys You can revoke API keys at any time from your workspace settings. Revocation is immediate and permanent—revoked keys cannot be restored or reactivated. Revoke keys immediately if you suspect they've been compromised, or when they're no longer needed. This is a critical security practice, especially when keys are used in production environments. > **Error: Security Alert** > If an API key is exposed or compromised, revoke it immediately and create a new one. Monitor your API usage logs for any suspicious activity after key revocation. --- ### Authentication Secure your API requests with Bearer token authentication. All endpoints require a valid API key in the Authorization header. Canonical URL: https://hoko.to/docs/authentication Markdown URL: https://hoko.to/docs/authentication/README.md ## Overview Hoko uses Bearer token authentication for all API requests. This industry-standard approach ensures secure access to your workspace data while keeping implementation simple. Every API request must include a valid API key in the Authorization header. API keys are scoped to specific permissions, allowing you to follow the principle of least privilege. ## Bearer Token Authentication Include your API key in every request using the Authorization header with the Bearer scheme. The API key should be placed immediately after "Bearer " (note the space). Both the full format "Bearer " and just the API key itself are accepted for convenience. **Header Format** ```text Authorization: Bearer ``` **curl Example** ```bash curl -X GET "https://hoko.to/api/links" \ -H "Authorization: Bearer " ``` **JavaScript (fetch)** ```javascript fetch('https://hoko.to/api/links', { headers: { "Authorization": "Bearer ", "Content-Type": "application/json" } }) ``` **Python (requests)** ```python import requests headers = { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' } response = requests.get('https://hoko.to/api/links', headers=headers) ``` > **Warning: Security Best Practice** > Never expose your API keys in client-side code, public repositories, or shared documents. Always store API keys securely using environment variables or secret management services. ## Authentication Errors When authentication fails, the API returns a 401 Unauthorized status code with a descriptive error message. Understanding these errors helps you troubleshoot authentication issues quickly. - Missing Authorization header - The request doesn't include an Authorization header - Invalid API key format - The API key format is incorrect or malformed - Invalid or revoked key - The API key doesn't exist, has been revoked, or belongs to a different workspace - Expired key - The API key has expired (if expiration is configured) **Error Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` > **Info: Troubleshooting** > If you receive a 401 error, verify that your API key is correct, hasn't been revoked, and is included in the Authorization header. Check your workspace settings to ensure the key is active and has the required scopes. --- ### Rate Limits Understand rate limits, monitor your usage, and implement proper retry logic to ensure reliable API access. Canonical URL: https://hoko.to/docs/rate-limits Markdown URL: https://hoko.to/docs/rate-limits/README.md ## Overview Rate limiting protects the API from abuse and ensures fair usage across all workspaces. All API requests are rate limited per workspace based on your subscription plan. Rate limits are applied using a sliding window algorithm per minute. This means requests are counted within a rolling 60-second window, providing smooth and predictable rate limiting behavior. Each plan tier has different rate limits to accommodate various usage patterns, from small projects to enterprise-scale operations. > **Info: Rate Limit Scope** > Rate limits are applied per workspace, not per API key. All API keys within the same workspace share the same rate limit pool. ## Rate Limit Headers Every API response includes rate limit information in HTTP headers, allowing you to monitor your usage and implement intelligent retry logic. These headers are always present, even when the request is successful. Use these headers to build rate limit-aware applications that can proactively manage their API usage. - X-RateLimit-Limit - The maximum number of requests allowed per minute for your plan tier - X-RateLimit-Remaining - The number of requests remaining in the current window - X-RateLimit-Reset - Unix timestamp (in seconds) indicating when the current rate limit window resets **Response Headers** ```text // Example response headers X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 987 X-RateLimit-Reset: 1704067200 ``` > **Tip: Implementation Tip** > Monitor the X-RateLimit-Remaining header and implement exponential backoff when it approaches zero. This prevents hitting rate limits and ensures smooth API usage. ## Handling Rate Limit Exceeded When you exceed your rate limit, the API returns a 429 Too Many Requests status code. The response includes detailed information to help you implement proper retry logic. The retryAfter field indicates the number of seconds you should wait before retrying the request. This value is calculated based on when the current rate limit window resets. **429 Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 45 } ``` > **Warning: Best Practice** > Always respect the retryAfter value and implement exponential backoff. Avoid aggressive retry loops that could further impact your rate limit. Consider implementing request queuing for high-volume scenarios. ## Best Practices To maximize API reliability and avoid rate limit issues, follow these best practices: - Monitor rate limit headers in every response to track your usage - Implement exponential backoff when approaching rate limits - Use batch endpoints when available to reduce the number of requests - Cache responses when appropriate to minimize API calls - Consider upgrading your plan if you consistently approach rate limits - Implement request queuing for high-volume operations - Use webhooks for real-time updates instead of polling when possible --- ### Errors Understand error responses, status codes, and how to handle API errors gracefully in your applications. Canonical URL: https://hoko.to/docs/errors Markdown URL: https://hoko.to/docs/errors/README.md ## Overview The Hoko API uses standard HTTP status codes to indicate the result of API requests. All error responses follow a consistent, predictable format that makes error handling straightforward in your applications. Error messages are provided in both English and Arabic, allowing you to display user-friendly error messages regardless of your application's language preference. Understanding error responses helps you build robust integrations that gracefully handle edge cases and provide clear feedback to users. ## HTTP Status Codes The API uses standard HTTP status codes to communicate request outcomes. Familiarize yourself with these codes to implement proper error handling: - 400 Bad Request - The request is malformed, contains invalid parameters, or violates API constraints. Check your request format and parameters. - 401 Unauthorized - Authentication failed. The API key is missing, invalid, expired, or revoked. Verify your API key is correct and active. - 403 Forbidden - The API key is valid but lacks the required permission scopes for this operation. Check the missingScopes field in the response. - 404 Not Found - Returned for unknown endpoints. Resource validation errors are reported as 400 Bad Request. - 429 Too Many Requests - Rate limit exceeded. Wait for the retryAfter period before making additional requests. - 500 Internal Server Error - An unexpected server error occurred. These are rare; if you encounter this, contact support with request details. > **Info: Error Handling Strategy** > Implement comprehensive error handling that checks status codes and displays appropriate messages to users. For 4xx errors, the issue is typically with the request. For 5xx errors, retry with exponential backoff. ## Error Response Format All error responses follow a consistent structure. The error object contains bilingual messages, and additional fields provide context-specific information depending on the error type. **Error Format** ```json { "error": { "en": "Error message in English", "ar": "رسالة خطأ بالعربية" }, "missingScopes": ["linksWrite"], // Only for 403 errors "retryAfter": 45 // Only for 429 errors } ``` > **Tip: Error Message Selection** > Use the appropriate language field (en or ar) from the error object based on your application's language preference or user settings. ## Error Examples Here are examples of common error responses you might encounter: **400 Bad Request** ```json { "error": { "en": "Invalid request format", "ar": "تنسيق طلب غير صالح" } } ``` **401 Unauthorized** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` **403 Forbidden** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["linksWrite", "collectionsWrite"] } ``` **429 Too Many Requests** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 45 } ``` --- ### Introduction The Links API is the foundation of Hoko, enabling you to create, manage, and track short links programmatically. Canonical URL: https://hoko.to/docs/links/introduction Markdown URL: https://hoko.to/docs/links/introduction/README.md ## Overview Links are the core building blocks of Hoko. Every short link you create becomes a trackable touchpoint that connects your marketing efforts to business outcomes. The Links API provides full CRUD operations (Create, Read, Update, Delete) for managing your link library. Create branded short URLs, organize them with collections, attach UTM parameters for campaign tracking, and leverage partner attribution for affiliate programs. Each link automatically generates a unique short ID and branded short URL, making it easy to share and track across all your marketing channels. Links support rich metadata including titles, descriptions, images, and custom external IDs for seamless integration with your existing systems. - (GET) Retrieve links with flexible filtering by ID, external ID, tenant, or partner - (POST) Create one or more links with rich metadata and UTM parameters - (PUT) Update existing links or create new ones with upsert operations - (DELETE) Soft delete links while preserving historical analytics data > **Tip: Getting Started** > Before creating links, ensure you have at least one collection in your workspace. Every link must belong to a collection for organization. Use externalId to maintain references to links in your own system for seamless synchronization. --- ### (GET) Get links Retrieve links from your workspace with flexible filtering options. Filter by link ID, external ID, tenant ID, or partner ID. Canonical URL: https://hoko.to/docs/links/get Markdown URL: https://hoko.to/docs/links/get/README.md ## Endpoint GET /api/links Retrieve links from your workspace with flexible filtering options. Filter by link ID, external ID, tenant ID, or partner ID to find exactly what you need. The endpoint supports efficient pagination and sorting, making it easy to browse large link libraries. Results are returned as an array of link objects, each containing complete link information including the generated short URL and QR code URL. **Endpoint** ```text GET /api/links ``` ## Authentication Requires authentication with an API key that has the linksRead scope. ## Query Parameters All parameters are optional. Use them to filter and paginate results. | Parameter | Type | Required | Location | Description | | ---------- | ------------- | -------- | -------- | ------------------------------------------------------------------------------------------- | | id | string (UUID) | No | query | Filter by specific link ID. Returns a single link if found. | | externalId | string | No | query | Filter by external ID. Useful for syncing with external systems. | | tenantId | string | No | query | Filter by tenant ID for multi-tenant scenarios. | | partnerId | string (UUID) | No | query | Filter by partner ID to retrieve all links associated with a specific partner. | | take | number | No | query | Number of results to return (default: 50, min: 1, max: 10,000). | | skip | number | No | query | Number of results to skip for pagination (default: 0, min: 0). | | sort | string | No | query | Sort order by createdAt: "asc" for oldest first, "desc" for newest first (default: "desc"). | > **Warning: Request Limits** > The `take` parameter supports values from 1 to 10,000. Requests above 10,000 are rejected. ## Status Codes ### 200 Successfully retrieved links. **Request** ```bash curl -X GET "https://hoko.to/api/links?take=50&sort=desc" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links?take=50&sort=desc', { method: 'GET', headers: { Authorization: 'Bearer ' } }); const links = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://example.com", "shortUrl": "https://hoko.to/abc123", "qrCode": "https://hoko.to/qrcode?text=https%3A%2F%2Fhoko.to%2Fabc123%3Fqr%3D1&size=512&errorCorrection=H&foreground=000000&background=FFFFFF&margin=1&format=svg", "title": "Example Link", "description": "An example link", "image": "https://example.com/image.jpg", "expiresAt": null, "expiredUrl": null, "password": null, "cloaked": false, "ios": null, "android": null, "geo": null, "createdAt": "2024-01-01T00:00:00Z" } ] ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X GET "https://hoko.to/api/links" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'GET', headers: { Authorization: 'Bearer invalid_key' } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required linksRead scope. **Request** ```bash curl -X GET "https://hoko.to/api/links" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'GET', headers: { Authorization: 'Bearer ' } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["linksRead"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-\* headers for details. **Request** ```bash curl -X GET "https://hoko.to/api/links" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'GET', headers: { Authorization: 'Bearer ' } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples Here are practical examples of retrieving links: **Request** ```bash curl -X GET "https://hoko.to/api/links?take=50&sort=desc" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links?take=50&sort=desc', { method: 'GET', headers: { Authorization: 'Bearer ' } }); const links = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://example.com", "shortUrl": "https://hoko.to/abc123", "qrCode": "https://hoko.to/qrcode?text=https%3A%2F%2Fhoko.to%2Fabc123%3Fqr%3D1&size=512&errorCorrection=H&foreground=000000&background=FFFFFF&margin=1&format=svg", "title": "Example Link", "description": "An example link", "image": "https://example.com/image.jpg", "utm": { "source": "google", "medium": "cpc", "campaign": "summer", "term": "keyword", "content": "ad_content" }, "collectionId": "550e8400-e29b-41d4-a716-446655440000", "externalId": "ext_123", "tenantId": "tenant_123", "partnerId": "550e8400-e29b-41d4-a716-446655440000", "createdAt": "2024-01-01T00:00:00Z" } ] ``` > **Info: Filtering Tips** > You can combine multiple filters (id, externalId, tenantId, partnerId) in a single request. When multiple filters are provided, results match any of the specified filters. Use pagination (take/skip) to efficiently browse large result sets. --- ### (POST) Create links Create one or more links in a single request. Each link requires a destination URL and collection ID. Canonical URL: https://hoko.to/docs/links/post Markdown URL: https://hoko.to/docs/links/post/README.md ## Endpoint POST /api/links Create one or more links in a single request. This endpoint accepts an array of link objects, allowing you to create multiple links efficiently in one API call. Each link requires a destination URL and a collection ID. A unique short ID is automatically generated for each link, producing a short URL and QR code URL under https://hoko.to. You can include rich metadata such as titles, descriptions, images, UTM parameters, and custom external IDs to organize and track your links effectively. **Endpoint** ```text POST /api/links ``` ## Authentication Requires authentication with an API key that has the linksWrite scope. ## Request Body The request body must be an array of link objects. Each object represents one link to create. You can create up to 10,000 links in a single request. However, the actual maximum may be lower based on your subscription plan limits and available resources. | Parameter | Type | Required | Location | Description | | ------------ | ------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------- | | url | string (URL) | Yes | body | The destination URL that the short link will redirect to. Must be a valid HTTP/HTTPS URL. | | collectionId | string (UUID) | Yes | body | The ID of the collection (folder) to organize this link. The collection must exist in your workspace. | | title | string | No | body | A descriptive title for the link. Useful for organization and identification. | | description | string | No | body | Additional description or notes about the link. | | image | string (URL) | No | body | A preview image URL for the link. Must be a valid HTTP/HTTPS URL. | | utm | object | No | body | UTM parameters for campaign tracking. Supports source, medium, campaign, term, content, and referral. | | expiresAt | string | No | body | ISO date-time when the link should expire. Requires a plan with link expiration. | | expiredUrl | string (URL) | No | body | Destination used after expiration. Requires a plan with link expiration. | | password | string | No | body | Password required before resolving the destination. Requires a plan with password protection. | | cloaked | boolean | No | body | Render the resolved destination in an iframe instead of redirecting. Requires a plan with link cloaking. | | ios | string (URL) | No | body | Destination for iOS visitors. Requires a plan with device targeting. | | android | string (URL) | No | body | Destination for Android visitors. Requires a plan with device targeting. | | geo | object | No | body | Country-code destination map such as `{ "SA": "https://example.com/sa" }`. Requires geo targeting. | | externalId | string | No | body | A custom external ID for syncing with external systems. Must be unique within your workspace. | | tenantId | string | No | body | A tenant identifier for multi-tenant scenarios. Useful for segmenting links by customer or organization. | | partnerId | string (UUID) | No | body | The ID of the partner associated with this link. Used for affiliate and partner attribution tracking. | > **Warning: Plan Limits** > The actual maximum number of links you can create per request may be lower than 10,000 based on your subscription plan limits and available resources. The system will enforce both the hard limit (10,000) and your plan-specific limits. ## Status Codes ### 201 Links created successfully. **Request** ```bash curl -X POST "https://hoko.to/api/links" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "url": "https://example.com", "collectionId": "550e8400-e29b-41d4-a716-446655440000", "expiresAt": "2026-12-31T23:59:00Z", "expiredUrl": "https://example.com/expired", "password": "launch", "cloaked": true, "ios": "https://example.com/ios", "android": "https://example.com/android", "geo": { "SA": "https://example.com/sa" } } ]' ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'POST', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify([ { url: 'https://example.com', collectionId: '550e8400-e29b-41d4-a716-446655440000', expiresAt: '2026-12-31T23:59:00Z', expiredUrl: 'https://example.com/expired', password: 'launch', cloaked: true, ios: 'https://example.com/ios', android: 'https://example.com/android', geo: { SA: 'https://example.com/sa' } } ]) }); const links = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://example.com", "shortUrl": "https://hoko.to/abc123", "qrCode": "https://hoko.to/qrcode?text=https%3A%2F%2Fhoko.to%2Fabc123%3Fqr%3D1&size=512&errorCorrection=H&foreground=000000&background=FFFFFF&margin=1&format=svg", "expiresAt": "2026-12-31T23:59:00Z", "expiredUrl": "https://example.com/expired", "password": "launch", "cloaked": true, "ios": "https://example.com/ios", "android": "https://example.com/android", "geo": { "SA": "https://example.com/sa" }, "createdAt": "2024-01-01T00:00:00Z" } ] ``` ### 400 Invalid request body (missing required fields or invalid values). **Request** ```bash curl -X POST "https://hoko.to/api/links" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "url": "invalid-url" } ]' ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'POST', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify([ { url: 'invalid-url' } ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X POST "https://hoko.to/api/links" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'POST', headers: { Authorization: 'Bearer invalid_key' } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required linksWrite scope. **Request** ```bash curl -X POST "https://hoko.to/api/links" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'POST', headers: { Authorization: 'Bearer ' } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["linksWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-\* headers for details. **Request** ```bash curl -X POST "https://hoko.to/api/links" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'POST', headers: { Authorization: 'Bearer ' } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X POST "https://hoko.to/api/links" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "url": "https://example.com", "title": "Example Link", "description": "An example link", "collectionId": "550e8400-e29b-41d4-a716-446655440000", "utm": { "source": "google", "medium": "cpc", "campaign": "summer" } } ]' ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'POST', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify([ { url: 'https://example.com', title: 'Example Link', description: 'An example link', collectionId: '550e8400-e29b-41d4-a716-446655440000', utm: { source: 'google', medium: 'cpc', campaign: 'summer' } } ]) }); const links = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://example.com", "shortUrl": "https://hoko.to/abc123", "qrCode": "https://hoko.to/qrcode?text=https%3A%2F%2Fhoko.to%2Fabc123%3Fqr%3D1&size=512&errorCorrection=H&foreground=000000&background=FFFFFF&margin=1&format=svg", "title": "Example Link", "description": "An example link", "collectionId": "550e8400-e29b-41d4-a716-446655440000", "createdAt": "2024-01-01T00:00:00Z" } ] ``` > **Warning: Important Constraints** > The collectionId must reference an existing collection in your workspace. The externalId must be unique within your workspace if provided. Plan limits may restrict the number of links you can create per request and overall. --- ### (PUT) Update/Upsert links Update existing links or create new ones with upsert operations. Perfect for synchronization scenarios. Canonical URL: https://hoko.to/docs/links/put Markdown URL: https://hoko.to/docs/links/put/README.md ## Endpoint PUT /api/links The PUT endpoint implements an upsert operation, allowing you to update existing links or create new ones in a single request. This is particularly useful for synchronization scenarios where you want to ensure links exist with specific properties. If an id is provided in the request body, the endpoint updates the existing link with that ID. If the ID does not exist, the request fails. To create new links, omit the id field. Returns the updated or created links in the same order as the input array, making it easy to correlate requests with responses in bulk operations. **Endpoint** ```text PUT /api/links ``` ## Authentication Requires authentication with an API key that has the linksWrite scope. ## Request Body The request body must be an array of link objects. Include the id field to update an existing link, or omit it to create a new one. You can upsert up to 10,000 links in a single request. However, the actual maximum may be lower based on your subscription plan limits and available resources. | Parameter | Type | Required | Location | Description | | ------------ | ------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------- | | id | string (UUID) | Yes | body | Required when updating. Omit to create a new link. If provided, it must exist in your workspace or the request fails. | | url | string (URL) | No | body | Destination URL. Required when creating a new link (no id). Optional when updating. | | collectionId | string (UUID) | No | body | Collection ID. Required when creating a new link (no id). Optional when updating. | | title | string | No | body | Link title. Only changes when provided. | | description | string | No | body | Link description. Only changes when provided. | | image | string (URL) | No | body | Preview image URL. Only changes when provided. | | utm | object | No | body | UTM parameters object. Only the provided UTM keys are updated. | | expiresAt | string/null | No | body | ISO date-time when the link should expire. Send null to clear. Requires link expiration when setting a value. | | expiredUrl | string/null | No | body | Destination used after expiration. Send null to clear. Requires link expiration when setting a value. | | password | string/null | No | body | Password required before resolving the destination. Send null to clear. Requires password protection when set. | | cloaked | boolean/null | No | body | Render the resolved destination in an iframe. Send false/null to clear. Requires link cloaking when true. | | ios | string/null | No | body | Destination for iOS visitors. Send null to clear. Requires device targeting when setting a value. | | android | string/null | No | body | Destination for Android visitors. Send null to clear. Requires device targeting when setting a value. | | geo | object/null | No | body | Country-code destination map. Send null to clear. Requires geo targeting when setting destinations. | | externalId | string | No | body | External ID for syncing. Must be unique in workspace when provided. | | tenantId | string | No | body | Tenant identifier. Optional. | | partnerId | string (UUID) | No | body | Partner ID for attribution. Optional. | > **Warning: Plan Limits** > The actual maximum number of links you can upsert per request may be lower than 10,000 based on your subscription plan limits and available resources. The system will enforce both the hard limit (10,000) and your plan-specific limits. ## Status Codes ### 200 Links updated or created successfully. **Request** ```bash curl -X PUT "https://hoko.to/api/links" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://updated.com", "title": "Updated Link", "expiresAt": "2026-12-31T23:59:00Z", "expiredUrl": "https://updated.com/expired", "cloaked": true, "ios": "https://updated.com/ios", "android": "https://updated.com/android", "geo": { "US": "https://updated.com/us" }, "collectionId": "550e8400-e29b-41d4-a716-446655440000" } ]' ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'PUT', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify([ { id: '550e8400-e29b-41d4-a716-446655440000', url: 'https://updated.com', title: 'Updated Link', expiresAt: '2026-12-31T23:59:00Z', expiredUrl: 'https://updated.com/expired', cloaked: true, ios: 'https://updated.com/ios', android: 'https://updated.com/android', geo: { US: 'https://updated.com/us' }, collectionId: '550e8400-e29b-41d4-a716-446655440000' } ]) }); const links = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://updated.com", "shortUrl": "https://hoko.to/abc123", "qrCode": "https://hoko.to/qrcode?text=https%3A%2F%2Fhoko.to%2Fabc123%3Fqr%3D1&size=512&errorCorrection=H&foreground=000000&background=FFFFFF&margin=1&format=svg", "title": "Updated Link", "expiresAt": "2026-12-31T23:59:00Z", "expiredUrl": "https://updated.com/expired", "cloaked": true, "ios": "https://updated.com/ios", "android": "https://updated.com/android", "geo": { "US": "https://updated.com/us" }, "createdAt": "2024-01-01T00:00:00Z" } ] ``` ### 400 Invalid request body (missing required fields or invalid values). **Request** ```bash curl -X PUT "https://hoko.to/api/links" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "url": "invalid-url" } ]' ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'PUT', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify([ { url: 'invalid-url' } ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X PUT "https://hoko.to/api/links" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'PUT', headers: { Authorization: 'Bearer invalid_key' } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required linksWrite scope. **Request** ```bash curl -X PUT "https://hoko.to/api/links" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'PUT', headers: { Authorization: 'Bearer ' } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["linksWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-\* headers for details. **Request** ```bash curl -X PUT "https://hoko.to/api/links" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'PUT', headers: { Authorization: 'Bearer ' } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X PUT "https://hoko.to/api/links" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://updated.com", "title": "Updated Link", "collectionId": "550e8400-e29b-41d4-a716-446655440000" } ]' ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/links', { method: 'PUT', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify([ { id: '550e8400-e29b-41d4-a716-446655440000', url: 'https://updated.com', title: 'Updated Link', collectionId: '550e8400-e29b-41d4-a716-446655440000' } ]) }); const links = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://updated.com", "shortUrl": "https://hoko.to/abc123", "qrCode": "https://hoko.to/qrcode?text=https%3A%2F%2Fhoko.to%2Fabc123%3Fqr%3D1&size=512&errorCorrection=H&foreground=000000&background=FFFFFF&margin=1&format=svg", "title": "Updated Link", "collectionId": "550e8400-e29b-41d4-a716-446655440000", "createdAt": "2024-01-01T00:00:00Z" } ] ``` > **Info: Upsert Behavior** > When updating, only the fields you include are changed and omitted fields keep their current values. To clear an optional field, send it explicitly as null. When creating a new link (no id), url and collectionId are required. --- ### (DELETE) Delete links Delete one or more links using soft delete. Historical analytics data is preserved. Canonical URL: https://hoko.to/docs/links/delete Markdown URL: https://hoko.to/docs/links/delete/README.md ## Endpoint DELETE /api/links Delete one or more links using soft delete. Soft delete marks links as deleted without permanently removing them from the database, allowing for potential recovery and maintaining referential integrity. Accepts an array of link IDs to delete. All specified links must belong to your workspace. The endpoint returns the IDs of successfully deleted links. Deleted links are no longer accessible via the API and will not appear in GET requests, but historical analytics data associated with these links is preserved. **Endpoint** ```text DELETE /api/links ``` ## Authentication Requires authentication with an API key that has the linksWrite scope. ## Request Body The request body must be an array of link IDs (UUIDs) to delete. You can delete up to 10,000 links in a single request. However, the actual maximum may be lower based on your subscription plan limits and available resources. | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | body | array | Yes | body | Array of link IDs (UUIDs) to delete. All IDs must be valid and belong to your workspace. | > **Warning: Request Size** > You can delete up to 10,000 links in a single request. Requests above 10,000 IDs are rejected. ## Status Codes ### 200 Links deleted successfully. **Request** ```bash curl -X DELETE "https://hoko.to/api/links" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "550e8400-e29b-41d4-a716-446655440000" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/links", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "550e8400-e29b-41d4-a716-446655440000" ]) }); const result = await response.json(); ``` **Response** ```json { "deletedIds": [ "550e8400-e29b-41d4-a716-446655440000" ] } ``` ### 400 Invalid request body (missing IDs, invalid UUIDs, or IDs not found in your workspace). **Request** ```bash curl -X DELETE "https://hoko.to/api/links" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "invalid-id" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/links", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "invalid-id" ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X DELETE "https://hoko.to/api/links" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/links", { method: "DELETE", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required linksWrite scope. **Request** ```bash curl -X DELETE "https://hoko.to/api/links" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/links", { method: "DELETE", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["linksWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X DELETE "https://hoko.to/api/links" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/links", { method: "DELETE", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X DELETE "https://hoko.to/api/links" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "550e8400-e29b-41d4-a716-446655440000" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/links", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "550e8400-e29b-41d4-a716-446655440000" ]) }); const result = await response.json(); ``` **Response** ```json { "deletedIds": [ "550e8400-e29b-41d4-a716-446655440000" ] } ``` > **Warning: Permanent Action** > Deleted links cannot be restored through the API. While the data is preserved for analytics purposes, the links themselves are no longer accessible. Ensure you have backups or exports before performing bulk deletions. --- ### Introduction Organize your links with collections. Collections are essential organizational units that help you structure and manage your link library. Canonical URL: https://hoko.to/docs/collections/introduction Markdown URL: https://hoko.to/docs/collections/introduction/README.md ## Overview Collections, provide a powerful way to organize and categorize your links. Think of collections as containers that help you group related links together—whether by campaign, project, client, or any other organizational structure that makes sense for your workflow. Every link must belong to a collection, making collections a fundamental organizational unit in Hoko. Use collections to create logical groupings that make it easier to find, manage, and analyze your links. The Collections API provides full CRUD operations for managing your organizational structure. Create collections before creating links, and use them to maintain a clean, organized link library. - (GET) Retrieve collections with pagination and filtering - (POST) Create one or more collections to organize your links - (PUT) Update existing collections or create new ones with upsert - (DELETE) Soft delete collections while preserving link relationships > **Tip: Organization Strategy** > Create collections before creating links. Plan your collection structure to match your workflow—consider organizing by campaign, product line, marketing channel, or client. You can always reorganize later, but a good initial structure saves time. --- ### (GET) Get collections Retrieve collections from your workspace. Filter by collection ID or list all collections with pagination. Canonical URL: https://hoko.to/docs/collections/get Markdown URL: https://hoko.to/docs/collections/get/README.md ## Endpoint GET /api/collections Retrieve collections from your workspace. Filter by collection ID to get specific collections, or omit the filter to retrieve all collections with pagination support. Use this endpoint to list your collections, verify collection existence before creating links, or build collection management interfaces. **Endpoint** ```text GET /api/collections ``` ## Authentication Requires authentication with an API key that has the collectionsRead scope. ## Query Parameters | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | id | string (UUID) | No | query | Filter by specific collection ID. Returns a single collection if found. | | take | number | No | query | Number of results to return (default: 50, min: 1, max: 10,000). | | skip | number | No | query | Number of results to skip for pagination (default: 0, min: 0). | | sort | string | No | query | Sort order by createdAt: "asc" for oldest first, "desc" for newest first (default: "desc"). | > **Warning: Request Limits** > The `take` parameter supports values from 1 to 10,000. Requests above 10,000 are rejected. ## Status Codes ### 200 Successfully retrieved collections. **Request** ```bash curl -X GET "https://hoko.to/api/collections?take=50&sort=desc" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections?take=50&sort=desc", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Marketing Campaigns", "description": "All marketing links", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X GET "https://hoko.to/api/collections" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "GET", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required collectionsRead scope. **Request** ```bash curl -X GET "https://hoko.to/api/collections" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["collectionsRead"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X GET "https://hoko.to/api/collections" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X GET "https://hoko.to/api/collections?take=50&sort=desc" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections?take=50&sort=desc", { method: "GET", headers: { "Authorization": "Bearer " } }); const collections = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Marketing Campaigns", "description": "All marketing links", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` --- ### (POST) Create collections Create one or more collections to organize your links. Each collection requires a name. Canonical URL: https://hoko.to/docs/collections/post Markdown URL: https://hoko.to/docs/collections/post/README.md ## Endpoint POST /api/collections Create one or more collections to organize your links. Collections are essential organizational units—every link must belong to a collection. Each collection requires a name for identification. Optionally include a description to provide additional context about the collection's purpose or contents. You can create multiple collections in a single request by providing an array of collection objects, making it easy to set up your organizational structure quickly. **Endpoint** ```text POST /api/collections ``` ## Authentication Requires authentication with an API key that has the collectionsWrite scope. ## Request Body The request body must be an array of collection objects. Each object represents one collection to create. You can create up to 10,000 collections in a single request. However, the actual maximum may be lower based on your subscription plan limits and available resources. | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | name | string | Yes | body | The name of the collection. Used for identification and organization. Choose descriptive names that clearly indicate the collection's purpose. | | description | string | No | body | Optional description providing additional context about the collection. Useful for documentation and team collaboration. | > **Warning: Plan Limits** > The actual maximum number of collections you can create per request may be lower than 10,000 based on your subscription plan limits and available resources. The system will enforce both the hard limit (10,000) and your plan-specific limits. ## Status Codes ### 201 Collections created successfully. **Request** ```bash curl -X POST "https://hoko.to/api/collections" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "name": "Marketing Campaigns" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { name: "Marketing Campaigns" } ]) }); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Marketing Campaigns", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` ### 400 Invalid request body (missing required fields or invalid values). **Request** ```bash curl -X POST "https://hoko.to/api/collections" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ {} ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ {} ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X POST "https://hoko.to/api/collections" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "POST", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required collectionsWrite scope. **Request** ```bash curl -X POST "https://hoko.to/api/collections" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "POST", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["collectionsWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X POST "https://hoko.to/api/collections" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "POST", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X POST "https://hoko.to/api/collections" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "name": "Marketing Campaigns", "description": "All marketing campaign links" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { name: "Marketing Campaigns", description: "All marketing campaign links" } ]) }); const collections = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Marketing Campaigns", "description": "All marketing campaign links", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` > **Info: Collection Requirements** > Every link must belong to a collection. Create collections before creating links, or ensure the collectionId you reference in link creation requests exists in your workspace. --- ### (PUT) Update/Upsert collections Update existing collections or create new ones with upsert operations. Canonical URL: https://hoko.to/docs/collections/put Markdown URL: https://hoko.to/docs/collections/put/README.md ## Endpoint PUT /api/collections Upsert (update or insert) one or more collections. If id is provided, the collection is updated. If the ID does not exist, the request fails. To create new collections, omit the id field. This endpoint is useful for synchronization scenarios where you want to ensure collections exist with specific properties. **Endpoint** ```text PUT /api/collections ``` ## Authentication Requires authentication with an API key that has the collectionsWrite scope. ## Request Body The request body must be an array of collection objects. Include the id field to update an existing collection, or omit it to create a new one. You can upsert up to 10,000 collections in a single request. However, the actual maximum may be lower based on your subscription plan limits and available resources. | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | id | string (UUID) | Yes | body | Required when updating. Omit to create a new collection. If provided, it must exist in your workspace or the request fails. | | name | string | No | body | The name of the collection. Required when creating a new collection (no id). Optional when updating. | | description | string | No | body | Optional description. Only changes when provided. | > **Info: Update Behavior** > When updating, only the fields you include are changed. Omitted fields keep their current values. To clear a value, send it explicitly as null. > **Warning: Plan Limits** > The actual maximum number of collections you can upsert per request may be lower than 10,000 based on your subscription plan limits and available resources. The system will enforce both the hard limit (10,000) and your plan-specific limits. ## Status Codes ### 200 Collections updated or created successfully. **Request** ```bash curl -X PUT "https://hoko.to/api/collections" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Name" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { id: "550e8400-e29b-41d4-a716-446655440000", name: "Updated Name" } ]) }); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Name", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` ### 400 Invalid request body (missing required fields or invalid values). **Request** ```bash curl -X PUT "https://hoko.to/api/collections" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ {} ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ {} ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X PUT "https://hoko.to/api/collections" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "PUT", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required collectionsWrite scope. **Request** ```bash curl -X PUT "https://hoko.to/api/collections" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "PUT", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["collectionsWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X PUT "https://hoko.to/api/collections" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "PUT", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X PUT "https://hoko.to/api/collections" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Name", "description": "Updated description" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { id: "550e8400-e29b-41d4-a716-446655440000", name: "Updated Name", description: "Updated description" } ]) }); const collections = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Name", "description": "Updated description", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` --- ### (DELETE) Delete collections Delete one or more collections using soft delete. Canonical URL: https://hoko.to/docs/collections/delete Markdown URL: https://hoko.to/docs/collections/delete/README.md ## Endpoint DELETE /api/collections Delete one or more collections using soft delete. Soft delete marks collections as deleted without permanently removing them from the database. Accepts an array of collection IDs to delete. All specified collections must belong to your workspace. **Endpoint** ```text DELETE /api/collections ``` ## Authentication Requires authentication with an API key that has the collectionsWrite scope. ## Request Body The request body must be an array of collection IDs (UUIDs) to delete. You can delete up to 10,000 collections in a single request. Requests above 10,000 IDs are rejected. | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | body | array | Yes | body | Array of collection IDs (UUIDs) to delete. All IDs must be valid and belong to your workspace. | > **Warning: Request Size** > Ensure the request body contains at least one valid collection ID. ## Status Codes ### 200 Collections deleted successfully. **Request** ```bash curl -X DELETE "https://hoko.to/api/collections" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "550e8400-e29b-41d4-a716-446655440000" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "550e8400-e29b-41d4-a716-446655440000" ]) }); ``` **Response** ```json { "deletedIds": ["550e8400-e29b-41d4-a716-446655440000"] } ``` ### 400 Invalid request body (missing IDs, invalid UUIDs, or IDs not found in your workspace). **Request** ```bash curl -X DELETE "https://hoko.to/api/collections" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "invalid-id" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "invalid-id" ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X DELETE "https://hoko.to/api/collections" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "DELETE", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required collectionsWrite scope. **Request** ```bash curl -X DELETE "https://hoko.to/api/collections" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "DELETE", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["collectionsWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X DELETE "https://hoko.to/api/collections" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "DELETE", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X DELETE "https://hoko.to/api/collections" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "550e8400-e29b-41d4-a716-446655440000" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/collections", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "550e8400-e29b-41d4-a716-446655440000" ]) }); const result = await response.json(); ``` **Response** ```json { "deletedIds": ["550e8400-e29b-41d4-a716-446655440000"] } ``` > **Warning: Important Note** > Deleted collections cannot be restored through the API. Links that belong to deleted collections will still exist but may need to be moved to active collections for proper organization. --- ### Introduction Create, read, update, and delete tags for categorizing and organizing your links. Canonical URL: https://hoko.to/docs/tags/introduction Markdown URL: https://hoko.to/docs/tags/introduction/README.md ## Overview Tags help categorize and filter your links, providing a flexible labeling system that complements collections. While collections organize links into folders, tags allow you to apply multiple labels to a single link, enabling cross-cutting categorization. Use tags to mark links by campaign type, content category, target audience, priority level, or any other dimension that helps you organize and find your links. A single link can have multiple tags, making it easy to filter and search across different organizational axes. The Tags API provides full CRUD operations for managing your tag library. Create tags with custom names and colors, update them as your needs evolve, and delete tags that are no longer needed. - (GET) Retrieve tags with pagination and filtering - (POST) Create one or more tags with custom names and colors - (PUT) Update existing tags or create new ones with upsert - (DELETE) Permanently delete tags (hard delete) > **Tip: Tag Strategy** > Plan your tag structure to complement your collection organization. Use tags for cross-cutting concerns that span multiple collections, such as campaign types, content categories, or priority levels. Choose descriptive tag names and use colors to visually distinguish different tag categories. --- ### (GET) Get tags Retrieve tags from your workspace. Filter by tag ID or list all tags with pagination. Canonical URL: https://hoko.to/docs/tags/get Markdown URL: https://hoko.to/docs/tags/get/README.md ## Endpoint GET /api/tags Retrieve tags by id. Supports pagination with take, skip, and sorting by tag id. Use this endpoint to list your tags, verify tag existence, or build tag management interfaces. **Endpoint** ```text GET /api/tags ``` ## Authentication Requires authentication with an API key that has the tagsRead scope. ## Query Parameters | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | id | string (UUID) | No | query | Filter by specific tag ID. Returns a single tag if found. | | take | number | No | query | Number of results to return (default: 50, min: 1, max: 10,000). | | skip | number | No | query | Number of results to skip for pagination (default: 0, min: 0). | | sort | string | No | query | Sort order by tag id: "asc" for oldest first, "desc" for newest first (default: "desc"). | > **Warning: Request Limits** > The `take` parameter supports values from 1 to 10,000. Requests above 10,000 are rejected. ## Status Codes ### 200 Successfully retrieved tags. **Request** ```bash curl -X GET "https://hoko.to/api/tags?take=50&sort=desc" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags?take=50&sort=desc", { method: "GET", headers: { "Authorization": "Bearer " } }); const tags = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Marketing", "color": "blue" } ] ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X GET "https://hoko.to/api/tags" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "GET", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required tagsRead scope. **Request** ```bash curl -X GET "https://hoko.to/api/tags" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["tagsRead"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X GET "https://hoko.to/api/tags" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X GET "https://hoko.to/api/tags?take=50&sort=desc" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags?take=50&sort=desc", { method: "GET", headers: { "Authorization": "Bearer " } }); const tags = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Marketing", "color": "blue" } ] ``` --- ### (POST) Create tags Create one or more tags. Each tag requires a name, and color is optional. Canonical URL: https://hoko.to/docs/tags/post Markdown URL: https://hoko.to/docs/tags/post/README.md ## Endpoint POST /api/tags Create one or more tags. Accepts an array of tag objects. Each tag requires a name. Color is optional (defaults to "slate"). **Endpoint** ```text POST /api/tags ``` ## Authentication Requires authentication with an API key that has the tagsWrite scope. ## Request Body The request body must be an array of tag objects. Each object represents one tag to create. You can create up to 10,000 tags in a single request. However, the actual maximum may be lower based on your subscription plan limits and available resources. | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | name | string | Yes | body | The name of the tag. Choose descriptive names that clearly indicate the tag's purpose. | | color | string | No | body | The color of the tag. Must be one of the available color values (default: "slate"). | > **Warning: Plan Limits** > The actual maximum number of tags you can create per request may be lower than 10,000 based on your subscription plan limits and available resources. The system will enforce both the hard limit (10,000) and your plan-specific limits. ## Status Codes ### 201 Tags created successfully. **Request** ```bash curl -X POST "https://hoko.to/api/tags" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "name": "Marketing", "color": "blue" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { name: "Marketing", color: "blue" } ]) }); const tags = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Marketing", "color": "blue" } ] ``` ### 400 Invalid request body (missing required fields or invalid values). **Request** ```bash curl -X POST "https://hoko.to/api/tags" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "name": "Marketing", "color": "invalid-color" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { name: "Marketing", color: "invalid-color" } ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X POST "https://hoko.to/api/tags" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "POST", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required tagsWrite scope. **Request** ```bash curl -X POST "https://hoko.to/api/tags" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "POST", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["tagsWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X POST "https://hoko.to/api/tags" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "POST", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples ### Available Colors Type: color - slate - gray - zinc - neutral - stone - red - orange - amber - yellow - lime - green - emerald - teal - cyan - sky - blue - indigo - violet - purple - fuchsia - pink - rose **Request** ```bash curl -X POST "https://hoko.to/api/tags" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "name": "Marketing", "color": "blue" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { name: "Marketing", color: "blue" } ]) }); const tags = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Marketing", "color": "blue" } ] ``` --- ### (PUT) Update/Upsert tags Update existing tags or create new ones with upsert operations. Canonical URL: https://hoko.to/docs/tags/put Markdown URL: https://hoko.to/docs/tags/put/README.md ## Endpoint PUT /api/tags Upsert (update or insert) one or more tags. If id is provided, the tag is updated. If the ID does not exist, the request fails. To create new tags, omit the id field. This endpoint is useful for synchronization scenarios where you want to ensure tags exist with specific properties. **Endpoint** ```text PUT /api/tags ``` ## Authentication Requires authentication with an API key that has the tagsWrite scope. ## Request Body The request body must be an array of tag objects. Include the id field to update an existing tag, or omit it to create a new one. You can upsert up to 10,000 tags in a single request. However, the actual maximum may be lower based on your subscription plan limits and available resources. | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | id | string (UUID) | Yes | body | Required when updating. Omit to create a new tag. If provided, it must exist in your workspace or the request fails. | | name | string | No | body | The name of the tag. Required when creating a new tag (no id). Optional when updating. | | color | string | No | body | The color of the tag. Optional. Defaults to \"slate\" when creating a new tag. | > **Info: Update Behavior** > When updating, only the fields you include are changed. Omitted fields keep their current values. For new tags (no id), color defaults to \"slate\" if omitted. > **Warning: Plan Limits** > The actual maximum number of tags you can upsert per request may be lower than 10,000 based on your subscription plan limits and available resources. The system will enforce both the hard limit (10,000) and your plan-specific limits. ## Status Codes ### 200 Tags updated or created successfully. **Request** ```bash curl -X PUT "https://hoko.to/api/tags" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Tag", "color": "green" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { id: "550e8400-e29b-41d4-a716-446655440000", name: "Updated Tag", color: "green" } ]) }); const tags = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Tag", "color": "green" } ] ``` ### 400 Invalid request body (missing required fields or invalid values). **Request** ```bash curl -X PUT "https://hoko.to/api/tags" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "name": "Tag", "color": "invalid-color" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { name: "Tag", color: "invalid-color" } ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X PUT "https://hoko.to/api/tags" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "PUT", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required tagsWrite scope. **Request** ```bash curl -X PUT "https://hoko.to/api/tags" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "PUT", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["tagsWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X PUT "https://hoko.to/api/tags" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "PUT", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples ### Available Colors Type: color - slate - gray - zinc - neutral - stone - red - orange - amber - yellow - lime - green - emerald - teal - cyan - sky - blue - indigo - violet - purple - fuchsia - pink - rose **Request** ```bash curl -X PUT "https://hoko.to/api/tags" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Tag", "color": "green" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { id: "550e8400-e29b-41d4-a716-446655440000", name: "Updated Tag", color: "green" } ]) }); const tags = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Tag", "color": "green" } ] ``` --- ### (DELETE) Delete tags Permanently delete one or more tags (hard delete). Canonical URL: https://hoko.to/docs/tags/delete Markdown URL: https://hoko.to/docs/tags/delete/README.md ## Endpoint DELETE /api/tags Delete one or more tags (hard delete). This permanently removes tags from your workspace. Accepts an array of tag IDs to delete. All specified tags must belong to your workspace. **Endpoint** ```text DELETE /api/tags ``` ## Authentication Requires authentication with an API key that has the tagsWrite scope. ## Request Body The request body must be an array of tag IDs (UUIDs) to delete. You can delete up to 10,000 tags in a single request. Requests above 10,000 IDs are rejected. | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | body | array | Yes | body | Array of tag IDs (UUIDs) to delete. All IDs must be valid and belong to your workspace. | > **Warning: Request Size** > Ensure the request body contains at least one valid tag ID. ## Status Codes ### 200 Tags deleted successfully. **Request** ```bash curl -X DELETE "https://hoko.to/api/tags" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "550e8400-e29b-41d4-a716-446655440000" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "550e8400-e29b-41d4-a716-446655440000" ]) }); const result = await response.json(); ``` **Response** ```json { "deletedIds": [ "550e8400-e29b-41d4-a716-446655440000" ] } ``` ### 400 Invalid request body (missing IDs, invalid UUIDs, or IDs not found in your workspace). **Request** ```bash curl -X DELETE "https://hoko.to/api/tags" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "invalid-id" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "invalid-id" ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X DELETE "https://hoko.to/api/tags" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "DELETE", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required tagsWrite scope. **Request** ```bash curl -X DELETE "https://hoko.to/api/tags" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "DELETE", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["tagsWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X DELETE "https://hoko.to/api/tags" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "DELETE", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X DELETE "https://hoko.to/api/tags" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "550e8400-e29b-41d4-a716-446655440000" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/tags", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "550e8400-e29b-41d4-a716-446655440000" ]) }); const result = await response.json(); ``` **Response** ```json { "deletedIds": [ "550e8400-e29b-41d4-a716-446655440000" ] } ``` > **Warning: Permanent Action** > Deleted tags cannot be restored. This is a hard delete operation that permanently removes tags from your workspace. Links that had these tags will no longer have those tag associations. --- ### Introduction Retrieve click analytics and performance data for your links. Gain insights into traffic patterns, user behavior, and campaign effectiveness. Canonical URL: https://hoko.to/docs/analytics/introduction Markdown URL: https://hoko.to/docs/analytics/introduction/README.md ## Overview The Analytics API provides read-only access to comprehensive click data and analytics for your links. Track every click, understand your audience, and measure campaign performance with detailed insights. Every click on your short links is automatically tracked and enriched with metadata including device information, geographic location, browser details, referrer data, and UTM parameters. This data is immediately available through the Analytics API for real-time analysis and reporting. Use the Analytics API to build custom dashboards, generate reports, analyze traffic patterns, measure campaign ROI, and understand how users interact with your links across different channels and devices. - (GET) Retrieve click analytics with flexible filtering by link, partner, or date range - Detailed click data including device, location, browser, and UTM parameters - Support for pagination and sorting to efficiently process large datasets - Real-time analytics data available immediately after clicks occur > **Info: Data Availability** > Analytics data is available immediately after a click occurs. Use date range filters to analyze specific time periods, and combine with link or partner filters to drill down into specific segments of your traffic. --- ### (GET) Get analytics Retrieve click analytics and performance data for your links. Filter by link, partner, or date range. Canonical URL: https://hoko.to/docs/analytics/get Markdown URL: https://hoko.to/docs/analytics/get/README.md ## Endpoint GET /api/analytics Retrieve click analytics and performance data for your links. Filter by link, partner, or date range to analyze specific segments of your traffic. Supports flexible filtering and pagination, making it easy to extract insights from large datasets. Results include detailed click information including device, location, browser, and UTM parameters. **Endpoint** ```text GET /api/analytics ``` ## Authentication Requires authentication with an API key that has the analyticsRead scope. ## Query Parameters | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | linkId | string (UUID) | No | query | Filter analytics by specific link ID. Returns click data only for the specified link. | | partnerId | string (UUID) | No | query | Filter analytics by partner ID. Returns click data for all links associated with the specified partner. | | startDate | string (ISO 8601) | No | query | Start date for the date range filter. Must be in ISO 8601 format (e.g., "2024-01-01T00:00:00Z"). Only clicks on or after this date are returned. | | endDate | string (ISO 8601) | No | query | End date for the date range filter. Must be in ISO 8601 format (e.g., "2024-01-31T23:59:59Z"). Only clicks on or before this date are returned. | | take | number | No | query | Number of results to return (default: 50, min: 1, max: 10,000). Use pagination to retrieve large datasets efficiently. | | skip | number | No | query | Number of results to skip for pagination (default: 0, min: 0). Use with take to implement pagination. | | sort | string | No | query | Sort order by createdAt: "asc" for oldest clicks first, "desc" for newest clicks first (default: "desc"). | > **Warning: Request Limits** > The `take` parameter supports values from 1 to 10,000. Requests above 10,000 are rejected. ## Status Codes ### 200 Successfully retrieved analytics data. **Request** ```bash curl -X GET "https://hoko.to/api/analytics?linkId=550e8400-e29b-41d4-a716-446655440000&take=50" \ -H "Authorization: Bearer " ``` **Request** ```javascript const params = new URLSearchParams({ linkId: "550e8400-e29b-41d4-a716-446655440000", take: "50" }); const response = await fetch(`https://hoko.to/api/analytics?${params}`, { method: "GET", headers: { "Authorization": "Bearer " } }); const analytics = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "linkId": "550e8400-e29b-41d4-a716-446655440000", "browserName": "Chrome", "deviceType": "desktop", "country": "United States", "createdAt": "2024-01-01T00:00:00Z" } ] ``` ### 400 Invalid query parameters (e.g., malformed dates or UUIDs). **Request** ```bash curl -X GET "https://hoko.to/api/analytics?startDate=invalid-date" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/analytics?startDate=invalid-date", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X GET "https://hoko.to/api/analytics" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/analytics", { method: "GET", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required analyticsRead scope. **Request** ```bash curl -X GET "https://hoko.to/api/analytics" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/analytics", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["analyticsRead"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X GET "https://hoko.to/api/analytics" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/analytics", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X GET "https://hoko.to/api/analytics?linkId=550e8400-e29b-41d4-a716-446655440000&startDate=2024-01-01T00:00:00Z&endDate=2024-01-31T23:59:59Z&take=50" \ -H "Authorization: Bearer " ``` **Request** ```javascript const params = new URLSearchParams({ linkId: "550e8400-e29b-41d4-a716-446655440000", startDate: "2024-01-01T00:00:00Z", endDate: "2024-01-31T23:59:59Z", take: "50" }); const response = await fetch(`https://hoko.to/api/analytics?${params}`, { method: "GET", headers: { "Authorization": "Bearer " } }); const analytics = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "linkId": "550e8400-e29b-41d4-a716-446655440000", "partnerId": "550e8400-e29b-41d4-a716-446655440000", "visitorId": "visitor_123", "visitorSessionId": "session_123", "referrer": "https://google.com", "destination": "https://example.com", "shortId": "abc123", "utm": { "source": "google", "medium": "cpc", "campaign": "summer" }, "externalId": "ext_123", "tenantId": "tenant_123", "browserName": "Chrome", "browserVersion": "120.0", "osName": "Windows", "osVersion": "10", "deviceType": "desktop", "country": "United States", "countryCode": "US", "city": "New York", "createdAt": "2024-01-01T00:00:00Z" } ] ``` --- ### Introduction Read customer data. Customer creation and updates are handled automatically through conversion tracking endpoints. Canonical URL: https://hoko.to/docs/customers/introduction Markdown URL: https://hoko.to/docs/customers/introduction/README.md ## Overview The Customers API provides read-only access to customer data. Customers are automatically created or updated when you track lead or sale conversion events, making it easy to maintain a synchronized customer database without manual intervention. When you track a lead or sale event using the conversion tracking endpoints (/api/track/lead or /api/track/sale), Hoko automatically creates or updates the customer record based on the customerExternalId. This ensures your customer data stays in sync with your conversion tracking activities. Use the Customers API to retrieve customer information, verify customer existence, build customer management interfaces, or integrate customer data with other systems. The API supports filtering by customer ID or external ID, making it easy to find specific customers. - (GET) Retrieve customers by ID or external ID with pagination support - Automatic customer creation/updates through conversion tracking endpoints - Read-only access—customer write operations handled via conversion tracking - Support for filtering and sorting to efficiently find specific customers > **Info: Customer Management** > Customer write operations (POST, PUT, DELETE) are not available through the Customers API. Customers are automatically created or updated when tracking lead or sale events using the conversionsWrite scope. This design ensures data consistency and simplifies your integration workflow. --- ### (GET) Get customers Retrieve customers by ID or external ID. Supports pagination and sorting. Canonical URL: https://hoko.to/docs/customers/get Markdown URL: https://hoko.to/docs/customers/get/README.md ## Endpoint GET /api/customers Retrieve customers by id or externalId. Supports pagination with take, skip, and sorting by createdAt. Use this endpoint to list your customers, verify customer existence, or build customer management interfaces. **Endpoint** ```text GET /api/customers ``` ## Authentication Requires authentication with an API key that has the customersRead scope. ## Query Parameters | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | id | string (UUID) | No | query | Filter by specific customer ID. Returns a single customer if found. | | externalId | string | No | query | Filter by external ID. Useful for syncing with external systems. | | take | number | No | query | Number of results to return (default: 50, min: 1, max: 10,000). | | skip | number | No | query | Number of results to skip for pagination (default: 0, min: 0). | | sort | string | No | query | Sort order by createdAt: "asc" for oldest first, "desc" for newest first (default: "desc"). | > **Warning: Request Limits** > The `take` parameter supports values from 1 to 10,000. Requests above 10,000 are rejected. ## Status Codes ### 200 Successfully retrieved customers. **Request** ```bash curl -X GET "https://hoko.to/api/customers?take=50&sort=desc" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/customers?take=50&sort=desc", { method: "GET", headers: { "Authorization": "Bearer " } }); const customers = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "externalId": "customer_123", "name": "John Doe", "email": "john@example.com", "phone": "+1234567890", "avatar": "https://example.com/avatar.jpg", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X GET "https://hoko.to/api/customers" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/customers", { method: "GET", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required customersRead scope. **Request** ```bash curl -X GET "https://hoko.to/api/customers" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/customers", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["customersRead"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X GET "https://hoko.to/api/customers" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/customers", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X GET "https://hoko.to/api/customers?take=50&sort=desc" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/customers?take=50&sort=desc", { method: "GET", headers: { "Authorization": "Bearer " } }); const customers = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "externalId": "customer_123", "name": "John Doe", "email": "john@example.com", "phone": "+1234567890", "avatar": "https://example.com/avatar.jpg", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` --- ### Introduction Create, read, update, and delete partners for attribution tracking and commission management. Canonical URL: https://hoko.to/docs/partners/introduction Markdown URL: https://hoko.to/docs/partners/introduction/README.md ## Overview Partners help track attribution and commission data for affiliate programs, referral systems, and partner marketing campaigns. Associate partners with links to automatically attribute clicks and conversions to the correct partner. Use partners to track which affiliates or referral sources are driving traffic and conversions, calculate commissions, and measure partner performance. Each partner can be associated with multiple links, enabling comprehensive attribution tracking across your marketing channels. The Partners API provides full CRUD operations for managing your partner database. Create partners, update their information, and track their performance through associated links and conversions. - (GET) Retrieve partners with pagination and filtering - (POST) Create one or more partners with contact information - (PUT) Update existing partners or create new ones with upsert - (DELETE) Soft delete partners while preserving attribution data > **Tip: Partner Attribution** > Associate partners with links using the partnerId field when creating or updating links. This enables automatic attribution of clicks and conversions to the correct partner, making it easy to track partner performance and calculate commissions. --- ### (GET) Get partners Retrieve partners by ID. Supports pagination and sorting. Canonical URL: https://hoko.to/docs/partners/get Markdown URL: https://hoko.to/docs/partners/get/README.md ## Endpoint GET /api/partners Retrieve partners by id. Supports pagination with take, skip, and sorting by createdAt. **Endpoint** ```text GET /api/partners ``` ## Authentication Requires authentication with an API key that has the partnersRead scope. ## Query Parameters | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | id | string (UUID) | No | query | Filter by specific partner ID. Returns a single partner if found. | | take | number | No | query | Number of results to return (default: 50, min: 1, max: 10,000). | | skip | number | No | query | Number of results to skip for pagination (default: 0, min: 0). | | sort | string | No | query | Sort order by createdAt: "asc" for oldest first, "desc" for newest first (default: "desc"). | > **Warning: Request Limits** > The `take` parameter supports values from 1 to 10,000. Requests above 10,000 are rejected. ## Status Codes ### 200 Successfully retrieved partners. **Request** ```bash curl -X GET "https://hoko.to/api/partners?take=50&sort=desc" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners?take=50&sort=desc", { method: "GET", headers: { "Authorization": "Bearer " } }); const partners = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Partner Name", "email": "partner@example.com", "phone": "+1234567890", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X GET "https://hoko.to/api/partners" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "GET", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required partnersRead scope. **Request** ```bash curl -X GET "https://hoko.to/api/partners" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["partnersRead"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X GET "https://hoko.to/api/partners" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "GET", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X GET "https://hoko.to/api/partners?take=50&sort=desc" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners?take=50&sort=desc", { method: "GET", headers: { "Authorization": "Bearer " } }); const partners = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Partner Name", "email": "partner@example.com", "phone": "+1234567890", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` --- ### (POST) Create partners Create one or more partners. Each partner requires a name. Canonical URL: https://hoko.to/docs/partners/post Markdown URL: https://hoko.to/docs/partners/post/README.md ## Endpoint POST /api/partners Create one or more partners. Accepts an array of partner objects. Each partner requires a name. Email and phone are optional. **Endpoint** ```text POST /api/partners ``` ## Authentication Requires authentication with an API key that has the partnersWrite scope. ## Request Body The request body must be an array of partner objects. Each object represents one partner to create. You can create up to 10,000 partners in a single request. Requests above 10,000 items are rejected. | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | name | string | Yes | body | The name of the partner. | | email | string (email) | No | body | Email address of the partner. | | phone | string | No | body | Phone number of the partner. | > **Warning: Request Size** > Ensure the request body contains at least one partner object. ## Status Codes ### 201 Partners created successfully. **Request** ```bash curl -X POST "https://hoko.to/api/partners" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "name": "Partner Name", "email": "partner@example.com", "phone": "+1234567890" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { name: "Partner Name", email: "partner@example.com", phone: "+1234567890" } ]) }); const partners = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Partner Name", "email": "partner@example.com", "phone": "+1234567890", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` ### 400 Invalid request body (missing required fields or invalid values). **Request** ```bash curl -X POST "https://hoko.to/api/partners" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ {} ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ {} ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X POST "https://hoko.to/api/partners" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "POST", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required partnersWrite scope. **Request** ```bash curl -X POST "https://hoko.to/api/partners" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "POST", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["partnersWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X POST "https://hoko.to/api/partners" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "POST", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X POST "https://hoko.to/api/partners" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "name": "Partner Name", "email": "partner@example.com", "phone": "+1234567890" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { name: "Partner Name", email: "partner@example.com", phone: "+1234567890" } ]) }); const partners = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Partner Name", "email": "partner@example.com", "phone": "+1234567890", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` --- ### (PUT) Update/Upsert partners Update existing partners or create new ones with upsert operations. Canonical URL: https://hoko.to/docs/partners/put Markdown URL: https://hoko.to/docs/partners/put/README.md ## Endpoint PUT /api/partners Upsert (update or insert) one or more partners. If id is provided, the partner is updated. If the ID does not exist, the request fails. To create new partners, omit the id field. **Endpoint** ```text PUT /api/partners ``` ## Authentication Requires authentication with an API key that has the linksWrite scope. ## Request Body The request body must be an array of partner objects. Include the id field to update an existing partner, or omit it to create a new one. You can upsert up to 10,000 partners in a single request. Requests above 10,000 items are rejected. | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | id | string (UUID) | Yes | body | Required when updating. Omit to create a new partner. If provided, it must exist in your workspace or the request fails. | | name | string | No | body | The name of the partner. Required when creating a new partner (no id). Optional when updating. | | email | string (email) | No | body | Email address of the partner. Optional for both create and update operations. | | phone | string | No | body | Phone number of the partner. Optional for both create and update operations. | > **Info: Update Behavior** > When updating, only the fields you include are changed. Omitted fields keep their current values. To clear a value, send it explicitly as null. > **Warning: Request Size** > Ensure the request body contains at least one partner object. ## Status Codes ### 200 Partners updated or created successfully. **Request** ```bash curl -X PUT "https://hoko.to/api/partners" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Partner", "email": "updated@example.com" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { id: "550e8400-e29b-41d4-a716-446655440000", name: "Updated Partner", email: "updated@example.com" } ]) }); const partners = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Partner", "email": "updated@example.com", "phone": null, "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` ### 400 Invalid request body (missing required fields or invalid values). **Request** ```bash curl -X PUT "https://hoko.to/api/partners" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ {} ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ {} ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X PUT "https://hoko.to/api/partners" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "PUT", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required linksWrite scope. **Request** ```bash curl -X PUT "https://hoko.to/api/partners" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "PUT", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["linksWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X PUT "https://hoko.to/api/partners" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "PUT", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X PUT "https://hoko.to/api/partners" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Partner", "email": "updated@example.com" } ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ { id: "550e8400-e29b-41d4-a716-446655440000", name: "Updated Partner", email: "updated@example.com" } ]) }); const partners = await response.json(); ``` **Response** ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Partner", "email": "updated@example.com", "phone": null, "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ] ``` --- ### (DELETE) Delete partners Delete one or more partners using soft delete. Canonical URL: https://hoko.to/docs/partners/delete Markdown URL: https://hoko.to/docs/partners/delete/README.md ## Endpoint DELETE /api/partners Delete one or more partners (soft delete). Accepts an array of partner IDs to delete. **Endpoint** ```text DELETE /api/partners ``` ## Authentication Requires authentication with an API key that has the linksWrite scope. ## Request Body The request body must be an array of partner IDs (UUIDs) to delete. You can delete up to 10,000 partners in a single request. Requests above 10,000 IDs are rejected. | Parameter | Type | Required | Location | Description | |---|---|---|---|---| | body | array | Yes | body | Array of partner IDs (UUIDs) to delete. All IDs must be valid and belong to your workspace. | > **Warning: Request Size** > Ensure the request body contains at least one valid partner ID. ## Status Codes ### 200 Partners deleted successfully. **Request** ```bash curl -X DELETE "https://hoko.to/api/partners" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "550e8400-e29b-41d4-a716-446655440000" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "550e8400-e29b-41d4-a716-446655440000" ]) }); const result = await response.json(); ``` **Response** ```json { "deletedIds": [ "550e8400-e29b-41d4-a716-446655440000" ] } ``` ### 400 Invalid request body (missing IDs, invalid UUIDs, or IDs not found in your workspace). **Request** ```bash curl -X DELETE "https://hoko.to/api/partners" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "invalid-id" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "invalid-id" ]) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X DELETE "https://hoko.to/api/partners" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "DELETE", headers: { "Authorization": "Bearer invalid_key" } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required linksWrite scope. **Request** ```bash curl -X DELETE "https://hoko.to/api/partners" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "DELETE", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["linksWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-* headers for details. **Request** ```bash curl -X DELETE "https://hoko.to/api/partners" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "DELETE", headers: { "Authorization": "Bearer " } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ## Examples **Request** ```bash curl -X DELETE "https://hoko.to/api/partners" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ "550e8400-e29b-41d4-a716-446655440000" ]' ``` **Request** ```javascript const response = await fetch("https://hoko.to/api/partners", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify([ "550e8400-e29b-41d4-a716-446655440000" ]) }); const result = await response.json(); ``` **Response** ```json { "deletedIds": [ "550e8400-e29b-41d4-a716-446655440000" ] } ``` --- ### Introduction Track lead and sale conversion events to connect marketing touchpoints to business outcomes. Automatically create and update customer records with flexible attribution tracking. Canonical URL: https://hoko.to/docs/track/introduction Markdown URL: https://hoko.to/docs/track/introduction/README.md ## Overview Conversion tracking is the bridge between your marketing efforts and business outcomes. Track when leads convert and sales happen, connecting every click to revenue and business value. Hoko provides embedded click tracking for owned pages, plus two conversion tracking endpoints: Track Lead for capturing lead events (form submissions, sign-ups, inquiries) and Track Sale for recording completed purchases or revenue-generating actions. Lead and sale endpoints automatically create or update customer records, ensuring your customer database stays synchronized with your conversion tracking activities. When you track a conversion event, Hoko automatically associates it with the click that led to the conversion, providing complete attribution from initial click to final conversion. This enables you to measure campaign ROI, understand customer journeys, and optimize your marketing spend based on actual business results. - Embedded click tracking - Record a click from an owned page without redirecting the visitor - (POST) Track Lead - Record lead conversion events and automatically create/update customer records - (POST) Track Sale - Record sale conversion events with revenue attribution and customer upsert - Automatic customer creation/updates based on customerExternalId - Flexible attribution linking conversions to clicks, links, and partners - Support for immediate and deferred tracking scenarios > **Tip: Getting Started** > Enable conversion tracking on your links, then send lead or sale events when conversions occur. Use customerExternalId to maintain references to customers in your own system, ensuring seamless synchronization between Hoko and your CRM or other systems. --- ### Browser attribution script (analytics.js) Persist the clickId from short-link redirects using analytics.js and send it to conversion endpoints. Canonical URL: https://hoko.to/docs/track/analytics-js Markdown URL: https://hoko.to/docs/track/analytics-js/README.md ## Overview The browser attribution script (`/analytics.js`) captures the `hoko_id` query parameter that Hoko appends to destination URLs and stores it as a cookie. This gives you a persistent clickId that you can send later when tracking leads or sales. ## What it does - Reads `hoko_id` from the current page URL - Stores it in a cookie named `hoko_id` for 90 days - Replaces the existing `hoko_id` cookie when the current page URL contains a new `hoko_id` - Does not send any network requests > **Info: Lightweight by design** > The script only writes the cookie. It does not call any tracking endpoints automatically. ## Add the script Include the script on pages where users land after clicking a Hoko short link. ```html ``` ## Read the cookie and send events Read the `hoko_id` cookie and pass it as `clickId` to conversion tracking endpoints. ```javascript const clickId = document.cookie .split('; ') .find((row) => row.startsWith('hoko_id=')) ?.split('=')[1]; await fetch('https://hoko.to/api/track/lead', { method: 'POST', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ clickId: clickId ?? '', eventName: 'Sign up', customerExternalId: 'customer_123', customerName: 'John Doe' }) }); ``` ## When it runs The script runs when the browser loads it. It must execute on the same domain where you want the cookie to be stored. --- ### Embedded click tracking Track a Hoko short-link click from an owned page without redirecting the visitor. Canonical URL: https://hoko.to/docs/track/embedded-click Markdown URL: https://hoko.to/docs/track/embedded-click/README.md ## Overview Embedded click tracking lets you record a normal Hoko click when a visitor lands on a page you control, without sending that visitor through the short-link redirect first. Use this when you already know which short link, referral code, partner code, or campaign should receive attribution on the page. ## How it works - Create a dedicated Hoko short link, such as `https://hoko.to/1234` - Add the embedded analytics script using that short ID - Hoko verifies that the page requesting the script is on the same hostname as the short-link destination - Hoko returns JavaScript with a short-lived signed capture token - The script reads or creates a first-party visitor session ID on the page, persists it for 90 days, then sends a signed capture request back to the same script URL - Hoko records the click only after that signed capture request is verified - The script first stores any `hoko_id` already present on the page URL - If the page URL does not include `hoko_id`, Hoko stores the generated embedded click ID as `hoko_id` - Conversion endpoints can later use `hoko_id` as the `clickId` > **Info: Redirect attribution wins first** > The embedded script includes the same page URL behavior as `/analytics.js`, so a `?hoko_id=...` from a normal short-link redirect takes priority. When there is no `hoko_id` on the page URL, the embedded click ID does not overwrite an existing `hoko_id` cookie. ## Add the script ```html ``` The short ID in the path identifies the Hoko link to track. Query parameters on the script URL are captured with the click. You do not need to include `https://hoko.to/analytics.js` alongside this script. The embedded script also persists `hoko_id` from normal short-link redirects. ## Host verification Embedded click tracking only records a click when the page requesting the script and the signed capture request are hosted on the same hostname as the short-link destination. For example, a short link that points to `https://example.com/pricing` can be embedded on `https://example.com/...` or `https://www.example.com/...`, but not on another website. If the browser does not send a `Referer` or `Origin` header, or if the hostname does not match, Hoko returns a no-op script and does not create a click. This protects clients from unrelated websites embedding their script and inflating their analytics. ## Supported attribution parameters | Parameter | Type | Required | Location | Description | | ------------ | ------ | -------- | -------- | --------------------------------------------------------------- | | shortId | string | Yes | path | The Hoko short ID in `/{shortId}/analytics.js`. | | utm_source | string | No | query | UTM source to store on the click. | | utm_medium | string | No | query | UTM medium to store on the click. | | utm_campaign | string | No | query | UTM campaign to store on the click. | | utm_term | string | No | query | UTM term to store on the click. | | utm_content | string | No | query | UTM content to store on the click. | | ref | string | No | query | Referral value to store on the click. | | referral | string | No | query | Alternative referral parameter. Used when `ref` is not present. | ## What gets captured The signed capture request is captured using the same short-link click pipeline as a redirect click. Hoko records request-level attributes such as timestamp, IP-derived location, visitor session ID, user agent, browser, operating system, device, language, referrer, and the UTM/referral parameters in the script URL. For embedded clicks, Hoko stores the browser's full current page URL as the click destination. The server still validates that this URL's hostname matches the configured short-link destination hostname before recording the click. ## Conversion attribution After the script runs, read the `hoko_id` cookie and pass it to conversion endpoints. ```javascript const clickId = document.cookie .split('; ') .find((row) => row.startsWith('hoko_id=')) ?.split('=')[1]; await fetch('https://hoko.to/api/track/lead', { method: 'POST', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ clickId: clickId ?? '', eventName: 'Sign up', customerExternalId: 'customer_123', customerName: 'John Doe' }) }); ``` ## Behavior notes - The response is not cached, because each script request creates a short-lived capture token. - The script does not redirect the visitor. - A `hoko_id` on the current page URL is stored the same way as `/analytics.js`, including replacing an older cookie value. - Without a page URL `hoko_id`, the embedded click ID does not overwrite an existing `hoko_id` cookie. - The script does not mark clicks as QR scans. Use the normal short-link redirect for QR tracking. - If JavaScript is blocked, the embedded click is not recorded and the `hoko_id` cookie is not stored. - Browser referrer policies may limit how much of the page URL Hoko receives. The embedded script needs either a `Referer` or `Origin` header for host verification. - Pass page-specific attribution in the script query string when needed. --- ### Track lead events Canonical URL: https://hoko.to/docs/track/lead Markdown URL: https://hoko.to/docs/track/lead/README.md ## Endpoint POST /api/track/lead Track a lead conversion event for a short link. This endpoint creates a lead event and upserts a customer record. **Endpoint** ```text POST /api/track/lead ``` ## Authentication Requires authentication with an API key that has the conversionsWrite scope. ## Request Body Accepts a single object (not an array). All fields are required unless marked optional. | Parameter | Type | Required | Location | Description | | ------------------ | -------------- | -------- | -------- | ------------------------------------------------------------------------------- | | clickId | string | Yes | body | Click ID to attribute the lead. Use an empty string ("") for deferred tracking. | | eventName | string | Yes | body | Lead event name (1-255 characters). | | customerExternalId | string | Yes | body | Customer external ID (1-100 characters). Used to upsert the customer. | | customerName | string | Yes | body | Customer name (max 100 characters). | | customerEmail | string (email) | No | body | Customer email. Optional. Can be null to clear. | | customerAvatar | string (URL) | No | body | Customer avatar URL. Optional. Can be null to clear. | | metadata | object | No | body | Optional metadata object stored with the lead event. | > **Info: Deferred Tracking** > Send clickId as an empty string ("") to attribute the lead to the customer's most recent lead click. This fails if the customer has no previous leads. > **Tip: Capture clickId in the browser** > If you don't already have a clickId, use the Browser attribution script to persist `hoko_id` and pass it to this endpoint. See [Browser attribution script](/docs/track/analytics-js). ## Status Codes ### 201 Lead event tracked successfully. **Request** ```bash curl -X POST "https://hoko.to/api/track/lead" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "clickId": "click_abc123", "eventName": "Sign up", "customerExternalId": "customer_123", "customerName": "John Doe", "customerEmail": "john@example.com" }' ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/track/lead', { method: 'POST', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ clickId: 'click_abc123', eventName: 'Sign up', customerExternalId: 'customer_123', customerName: 'John Doe', customerEmail: 'john@example.com' }) }); const result = await response.json(); ``` **Response** ```json { "click": { "id": "click_abc123" }, "link": { "id": "link_xyz", "url": "https://example.com", "shortUrl": "https://hoko.to/abc123", "qrCode": "https://hoko.to/qrcode?text=https%3A%2F%2Fhoko.to%2Fabc123%3Fqr%3D1&size=512&errorCorrection=H&foreground=000000&background=FFFFFF&margin=1&format=svg", "title": null, "description": null, "image": null, "utm": null, "collectionId": "550e8400-e29b-41d4-a716-446655440000", "partnerId": "partner_123", "tenantId": "tenant_456", "externalId": "ext_789", "createdAt": "2024-01-01T00:00:00Z" }, "lead": { "eventName": "Sign up", "metadata": { "source": "website" } }, "customer": { "id": "customer_123", "name": "John Doe", "email": "john@example.com", "phone": null, "avatar": "https://example.com/avatar.jpg", "externalId": "customer_123" } } ``` ### 400 Invalid request body, missing required fields, or invalid clickId/deferred tracking. **Request** ```bash curl -X POST "https://hoko.to/api/track/lead" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "clickId": "invalid-click-id", "eventName": "Sign up", "customerExternalId": "customer_123", "customerName": "John Doe" }' ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/track/lead', { method: 'POST', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ clickId: 'invalid-click-id', eventName: 'Sign up', customerExternalId: 'customer_123', customerName: 'John Doe' }) }); ``` **Response** ```json { "error": { "en": "Click ID invalid-click-id not found or does not belong to your workspace", "ar": "معرف النقرة invalid-click-id غير موجود أو لا ينتمي إلى مساحة العمل الخاصة بك" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X POST "https://hoko.to/api/track/lead" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/track/lead', { method: 'POST', headers: { Authorization: 'Bearer invalid_key' } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required conversionsWrite scope. **Request** ```bash curl -X POST "https://hoko.to/api/track/lead" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/track/lead', { method: 'POST', headers: { Authorization: 'Bearer ' } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["conversionsWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-\* headers for details. **Request** ```bash curl -X POST "https://hoko.to/api/track/lead" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/track/lead', { method: 'POST', headers: { Authorization: 'Bearer ' } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` --- ### Track sale events Canonical URL: https://hoko.to/docs/track/sale Markdown URL: https://hoko.to/docs/track/sale/README.md ## Endpoint POST /api/track/sale Track a sale conversion event for a short link. This endpoint creates a sale event and upserts a customer record. **Endpoint** ```text POST /api/track/sale ``` ## Authentication Requires authentication with an API key that has the conversionsWrite scope. ## Request Body Accepts a single object (not an array). All fields are required unless marked optional. | Parameter | Type | Required | Location | Description | | ------------------ | -------------- | -------- | -------- | --------------------------------------------------------------------- | | customerExternalId | string | Yes | body | Customer external ID (1-100 characters). Used to upsert the customer. | | amount | integer | Yes | body | Sale amount (non-negative integer). | | currency | string | No | body | Currency code. Defaults to "usd". | | eventName | string | No | body | Sale event name. Defaults to "Purchase" (max 255 characters). | | metadata | object | No | body | Optional metadata object stored with the sale event. | | leadEventName | string | No | body | Lead event name to attribute this sale to a specific lead. | | clickId | string | No | body | Click ID for direct sale attribution. | | customerName | string | Yes | body | Customer name (max 100 characters). | | customerEmail | string (email) | No | body | Customer email. Optional. Can be null to clear. | | customerAvatar | string (URL) | No | body | Customer avatar URL. Optional. Can be null to clear. | > **Tip: Capture clickId in the browser** > If you want to attribute a sale directly to a click, use the Browser attribution script to persist `hoko_id` and pass it as clickId. See [Browser attribution script](/docs/track/analytics-js). > **Info: Attribution Priority** > The sale is attributed in this order: leadEventName (if provided) → clickId (if provided) → most recent lead for the customer. If no lead exists and no clickId is provided, the request fails. ## Status Codes ### 201 Sale event tracked successfully. **Request** ```bash curl -X POST "https://hoko.to/api/track/sale" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "customerExternalId": "customer_123", "amount": 12900, "currency": "usd", "eventName": "Invoice paid", "customerName": "John Doe", "customerEmail": "john@example.com" }' ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/track/sale', { method: 'POST', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ customerExternalId: 'customer_123', amount: 12900, currency: 'usd', eventName: 'Invoice paid', customerName: 'John Doe', customerEmail: 'john@example.com' }) }); const result = await response.json(); ``` **Response** ```json { "click": { "id": "click_abc123" }, "link": { "id": "link_xyz", "url": "https://example.com", "shortUrl": "https://hoko.to/abc123", "qrCode": "https://hoko.to/qrcode?text=https%3A%2F%2Fhoko.to%2Fabc123%3Fqr%3D1&size=512&errorCorrection=H&foreground=000000&background=FFFFFF&margin=1&format=svg", "title": null, "description": null, "image": null, "utm": null, "collectionId": "550e8400-e29b-41d4-a716-446655440000", "partnerId": "partner_123", "tenantId": "tenant_456", "externalId": "ext_789", "createdAt": "2024-01-01T00:00:00Z" }, "customer": { "id": "customer_123", "name": "John Doe", "email": "john@example.com", "phone": null, "avatar": null, "externalId": "customer_123" }, "sale": { "amount": 12900, "currency": "usd", "eventName": "Invoice paid", "metadata": { "invoiceId": "inv_123" } } } ``` ### 400 Invalid request body, negative amount, or missing attribution data. **Request** ```bash curl -X POST "https://hoko.to/api/track/sale" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "customerExternalId": "customer_123", "amount": -100, "customerName": "John Doe" }' ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/track/sale', { method: 'POST', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ customerExternalId: 'customer_123', amount: -100, customerName: 'John Doe' }) }); ``` **Response** ```json { "error": { "en": "Invalid request", "ar": "طلب غير صالح" } } ``` ### 401 Invalid or missing API key. **Request** ```bash curl -X POST "https://hoko.to/api/track/sale" \ -H "Authorization: Bearer invalid_key" ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/track/sale', { method: 'POST', headers: { Authorization: 'Bearer invalid_key' } }); ``` **Response** ```json { "error": { "en": "Invalid API key", "ar": "مفتاح API غير صالح" } } ``` ### 403 API key does not have the required conversionsWrite scope. **Request** ```bash curl -X POST "https://hoko.to/api/track/sale" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/track/sale', { method: 'POST', headers: { Authorization: 'Bearer ' } }); ``` **Response** ```json { "error": { "en": "Missing required scopes", "ar": "الصلاحيات المطلوبة مفقودة" }, "missingScopes": ["conversionsWrite"] } ``` ### 429 Rate limit exceeded. Check X-RateLimit-\* headers for details. **Request** ```bash curl -X POST "https://hoko.to/api/track/sale" \ -H "Authorization: Bearer " ``` **Request** ```javascript const response = await fetch('https://hoko.to/api/track/sale', { method: 'POST', headers: { Authorization: 'Bearer ' } }); ``` **Response** ```json { "error": { "en": "Rate limit exceeded", "ar": "تم تجاوز حد المعدل" }, "retryAfter": 60 } ``` ---